Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
75.58% covered (warning)
75.58%
291 / 385
12.50% covered (danger)
12.50%
1 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
ZObjectFactory
75.58% covered (warning)
75.58%
291 / 385
12.50% covered (danger)
12.50%
1 / 8
132.34
0.00% covered (danger)
0.00%
0 / 1
 getLogger
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 createPersistentContent
80.68% covered (warning)
80.68%
71 / 88
0.00% covered (danger)
0.00%
0 / 1
11.87
 validatePersistentKeys
82.14% covered (warning)
82.14%
23 / 28
0.00% covered (danger)
0.00%
0 / 1
4.09
 create
77.36% covered (warning)
77.36%
82 / 106
0.00% covered (danger)
0.00%
0 / 1
21.76
 createKeyValues
86.05% covered (warning)
86.05%
37 / 43
0.00% covered (danger)
0.00%
0 / 1
12.39
 extractInnerObject
16.67% covered (danger)
16.67%
2 / 12
0.00% covered (danger)
0.00%
0 / 1
4.31
 extractObjectType
69.39% covered (warning)
69.39%
68 / 98
0.00% covered (danger)
0.00%
0 / 1
19.62
 trackSelfReference
75.00% covered (warning)
75.00%
6 / 8
0.00% covered (danger)
0.00%
0 / 1
5.39
1<?php
2/**
3 * WikiLambda ZObject factory
4 *
5 * @file
6 * @ingroup Extensions
7 * @copyright 2020– Abstract Wikipedia team; see AUTHORS.txt
8 * @license MIT
9 */
10
11namespace MediaWiki\Extension\WikiLambda;
12
13use MediaWiki\Extension\WikiLambda\Registry\ZErrorTypeRegistry;
14use MediaWiki\Extension\WikiLambda\Registry\ZTypeRegistry;
15use MediaWiki\Extension\WikiLambda\ZObjects\ZFunctionCall;
16use MediaWiki\Extension\WikiLambda\ZObjects\ZObject;
17use MediaWiki\Extension\WikiLambda\ZObjects\ZPersistentObject;
18use MediaWiki\Extension\WikiLambda\ZObjects\ZReference;
19use MediaWiki\Extension\WikiLambda\ZObjects\ZString;
20use MediaWiki\Extension\WikiLambda\ZObjects\ZType;
21use MediaWiki\Extension\WikiLambda\ZObjects\ZTypedList;
22use MediaWiki\Logger\LoggerFactory;
23use MediaWiki\Title\Title;
24use Psr\Log\LoggerInterface;
25
26class ZObjectFactory {
27
28    private static ?LoggerInterface $logger = null;
29
30    /**
31     * Because much of the uses of this class is static, we can't just create the logger in the
32     * constructor like we do elsewhere, so instead we have a static LoggerInterface instance.
33     */
34    private static function getLogger(): LoggerInterface {
35        self::$logger ??= LoggerFactory::getInstance( 'WikiLambda' );
36        return self::$logger;
37    }
38
39    /**
40     * Validates and creates a ZPersistentObject from the given input data.
41     * If the input already has the ZPersistentObject keys, it uses
42     * them to construct the wrapper object. If not, it builds a wrapper
43     * ZPersistentObject with empty values. The resulting ZObject will be
44     * structurally valid or well-formed.
45     *
46     * This method is the entrypoint from WikiLambda content object.
47     *
48     * TODO (T375065): This is probably unnecessary now. We never create an object
49     * just passing the inner object, so we should probably strip this feature.
50     *
51     * @param string|array|\stdClass $input The item to turn into a ZObject
52     * @return ZPersistentObject
53     * @throws ZErrorException
54     */
55    public static function createPersistentContent( $input ) {
56        // 1. Get ZObject type. If not present or invalid, throw a not wellformed error
57        try {
58            $inputTypeZObject = self::extractObjectType( $input );
59            $typeZid = $inputTypeZObject->getZValue();
60        } catch ( ZErrorException $e ) {
61            self::getLogger()->info(
62                __METHOD__ . ': Parsing input failed: {message}',
63                [ 'message' => $e->getMessage() ]
64            );
65            throw new ZErrorException(
66                ZErrorFactory::createValidationZError( $e->getZError() )
67            );
68        }
69
70        $object = $input;
71
72        // 2. If ZObject type is Z2/Z_PERSISTENT_OBJECT, get the inner object.
73        // If not present, throw a not wellformed error.
74        if ( $typeZid === ZTypeRegistry::Z_PERSISTENTOBJECT ) {
75            try {
76                $object = self::extractInnerObject( $input );
77            } catch ( ZErrorException $e ) {
78                self::getLogger()->info(
79                    __METHOD__ . ': type loading failed: {message}',
80                    [ 'message' => $e->getMessage() ]
81                );
82                throw new ZErrorException(
83                    ZErrorFactory::createValidationZError( $e->getZError() )
84                );
85            }
86            // Get type of the inner ZObject. If not present, throw a not wellformed error
87            try {
88                $innerTypeZObject = self::extractObjectType( $object );
89                $typeZid = $innerTypeZObject->getZValue();
90            } catch ( ZErrorException $e ) {
91                self::getLogger()->info(
92                    __METHOD__ . ': type parsing failed: {message}',
93                    [ 'message' => $e->getMessage() ]
94                );
95                throw new ZErrorException(
96                    ZErrorFactory::createValidationZError( $e->getZError() )
97                );
98            }
99        }
100
101        // 3. Make sure that the ZObject type is not one of the disallowed types
102        // to directly wrap in a ZPersistentObject
103        if ( in_array( $typeZid, ZTypeRegistry::DISALLOWED_ROOT_ZOBJECTS ) ) {
104            self::getLogger()->info(
105                __METHOD__ . ': Disallowed root ZObject type attempted: {typeZid}',
106                [ 'typeZid' => $typeZid ]
107            );
108            throw new ZErrorException(
109                ZErrorFactory::createZErrorInstance(
110                    ZErrorTypeRegistry::Z_ERROR_DISALLOWED_ROOT_ZOBJECT,
111                    [
112                        'data' => $object
113                    ]
114                )
115            );
116        }
117
118        // 4. Create ZPersistentObject wrapper
119        // 4.1. Extract persistent keys or assign empty values
120        $persistentId = null;
121        $persistentLabel = null;
122        $persistentAliases = null;
123        $persistentDescription = null;
124        if ( $inputTypeZObject->getZValue() === ZTypeRegistry::Z_PERSISTENTOBJECT ) {
125            // Check that required keys exist
126            try {
127                self::validatePersistentKeys( $input );
128            } catch ( ZErrorException $e ) {
129                self::getLogger()->info(
130                    __METHOD__ . ': validating key failed: {message}',
131                    [ 'message' => $e->getMessage() ]
132                );
133                throw new ZErrorException(
134                    ZErrorFactory::createValidationZError( $e->getZError() )
135                );
136            }
137            // Build the values
138            $persistentId = self::create( $input->{ ZTypeRegistry::Z_PERSISTENTOBJECT_ID } );
139            $persistentLabel = self::create( $input->{ ZTypeRegistry::Z_PERSISTENTOBJECT_LABEL } );
140
141            $persistentAliases = property_exists( $input, ZTypeRegistry::Z_PERSISTENTOBJECT_ALIASES )
142                ? self::create( $input->{ ZTypeRegistry::Z_PERSISTENTOBJECT_ALIASES } )
143                : null;
144
145            $persistentDescription = property_exists( $input,
146                ZTypeRegistry::Z_PERSISTENTOBJECT_DESCRIPTION )
147                ? self::create( $input->{ ZTypeRegistry::Z_PERSISTENTOBJECT_DESCRIPTION } )
148                : null;
149        }
150
151        // Build empty values if we are creating a new ZPersistentObject wrapper
152        // TODO (T362249): Looks like this case is never really used: contemplate removing it
153        $persistentId ??= self::create( ZTypeRegistry::Z_NULL_REFERENCE );
154        $persistentLabel ??= self::create( (object)[
155            ZTypeRegistry::Z_OBJECT_TYPE => ZTypeRegistry::Z_MULTILINGUALSTRING,
156            ZTypeRegistry::Z_MULTILINGUALSTRING_VALUE => [ ZTypeRegistry::Z_MONOLINGUALSTRING ]
157        ] );
158
159        // 4.2 Track self-reference if Z_PERSISTENT_ID is present
160        self::trackSelfReference( $persistentId->getZValue(), self::SET_SELF_ZID );
161
162        // 4.3. Create and validate inner ZObject: can throw Z502/Not wellformed
163        $zObject = self::create( $object );
164
165        // 4.5. Construct ZPersistentObject()
166        $persistentObject = new ZPersistentObject( $persistentId, $zObject, $persistentLabel,
167            $persistentAliases, $persistentDescription );
168
169        // 4.6. Check validity, to make sure that ID, label and aliases have the right format
170        if ( !$persistentObject->isValid() ) {
171            self::getLogger()->info( __METHOD__ . ': ZPersistentObject keys failed validation' );
172            throw new ZErrorException(
173                // TODO (T300506): Detail persistent object-related errors
174                ZErrorFactory::createZErrorInstance(
175                    ZErrorTypeRegistry::Z_ERROR_UNKNOWN,
176                    [
177                        'message' => "ZPersistentObject not valid"
178                    ]
179                )
180            );
181        }
182
183        // 4.6. Untrack self-reference
184        self::trackSelfReference( $persistentId->getZValue(), self::UNSET_SELF_ZID );
185        return $persistentObject;
186    }
187
188    /**
189     * Check that the required ZPersistentObject keys exists and, if they don't, raise
190     * Z511/Missing key errors
191     *
192     * @param string|array|\stdClass $input The item to check is a ZObject
193     * @return bool
194     * @throws ZErrorException
195     */
196    public static function validatePersistentKeys( $input ): bool {
197        // We have a different error for Z2K2 being missing vs. the others.
198        if ( !property_exists( $input, ZTypeRegistry::Z_PERSISTENTOBJECT_VALUE ) ) {
199            self::getLogger()->info( __METHOD__ . ': Input missing Z_PERSISTENTOBJECT_VALUE key' );
200            throw new ZErrorException( ZErrorFactory::createZErrorInstance(
201                ZErrorTypeRegistry::Z_ERROR_MISSING_PERSISTENT_VALUE,
202                [ 'data' => $input ],
203            ) );
204        }
205
206        $otherRequiredKeys = [
207            ZTypeRegistry::Z_PERSISTENTOBJECT_ID,
208            ZTypeRegistry::Z_PERSISTENTOBJECT_LABEL,
209            // Disabled for now, optional
210            // ZTypeRegistry::Z_PERSISTENTOBJECT_ALIASES,
211            // Disabled for now, optional
212            // ZTypeRegistry::Z_PERSISTENTOBJECT_DESCRIPTION,
213        ];
214
215        foreach ( $otherRequiredKeys as $_i => $requiredKey ) {
216            if ( !property_exists( $input, $requiredKey ) ) {
217                self::getLogger()->info(
218                    __METHOD__ . ': Input missing required key {key}',
219                    [ 'key' => $requiredKey ]
220                );
221                throw new ZErrorException( ZErrorFactory::createZErrorInstance(
222                    ZErrorTypeRegistry::Z_ERROR_MISSING_KEY,
223                    [
224                        'data' => $input,
225                        'keywordArgs' => [ 'missing' => $requiredKey ],
226                    ]
227                ) );
228            }
229        }
230
231        return true;
232    }
233
234    /**
235     * Creates an object of type ZObject from the given input. This method should only
236     * be called internally, either from the ZObjectFactory of from the ZObject
237     * constructors. ZObjects created using this method will not necessarily be
238     * structurally valid.
239     *
240     * @param string|array|ZObject|\stdClass $object The item to turn into a ZObject
241     * @return ZObject
242     * @throws ZErrorException
243     */
244    public static function create( $object ) {
245        if ( $object instanceof ZObject ) {
246            return $object;
247        }
248
249        if ( is_string( $object ) ) {
250            if ( ZObjectUtils::isValidZObjectReference( $object ) ) {
251                return new ZReference( $object );
252            } else {
253                return new ZString( $object );
254            }
255        }
256
257        if ( is_array( $object ) ) {
258            if ( count( $object ) === 0 ) {
259                self::getLogger()->info( __METHOD__ . ': Input array empty' );
260                throw new ZErrorException(
261                    ZErrorFactory::createZErrorInstance(
262                        ZErrorTypeRegistry::Z_ERROR_UNDEFINED_LIST_TYPE,
263                        [
264                            'data' => $object
265                        ]
266                    )
267                );
268            }
269
270            $rawListType = array_shift( $object );
271            $listType = self::create( $rawListType );
272
273            // TODO (T330321): All of the checks in the following if block have already been
274            // checked during static validation, but the following block is:
275            //  A) Incomplete (lacks other possible resolvers)
276            //  B) Doesn't check that the objects resolve to ZType (not sure if we wanna do that)
277            // So either we remove it completely, or we fix B.
278            if ( !(
279                // Mostly we expect direct references to ZTypes (but we don't check it's a type)
280                $listType instanceof ZReference ||
281                // … sometimes it's a ZFunctionCall to make a ZType (but we don't check it's a type)
282                $listType instanceof ZFunctionCall ||
283                // … occasionally it's an inline ZType (or a dereferenced one)
284                $listType instanceof ZType
285            ) ) {
286                self::getLogger()->info(
287                    __METHOD__ . ': Input list type is not allowed: {rawListType}',
288                    [ 'rawListType' => $rawListType ]
289                );
290                throw new ZErrorException(
291                    ZErrorFactory::createZErrorInstance(
292                        ZErrorTypeRegistry::Z_ERROR_WRONG_LIST_TYPE,
293                        [
294                            'data' => $rawListType
295                        ]
296                    )
297                );
298            }
299
300            $items = [];
301
302            foreach ( $object as $index => $value ) {
303                try {
304                    $item = self::create( $value );
305                } catch ( ZErrorException $e ) {
306                    // We increment the index to point at the correct array item
307                    // because we removed the first element by doing array_shift
308                    $arrayIndex = $index + 1;
309                    self::getLogger()->info(
310                        __METHOD__ . ': Key {index} broken: {message}',
311                        [ 'index' => $arrayIndex, 'message' => $e->getMessage() ]
312                    );
313                    throw new ZErrorException(
314                        ZErrorFactory::createArrayElementZError( (string)( $arrayIndex ), $e->getZError() )
315                    );
316                }
317                $items[] = $item;
318            }
319
320            return new ZTypedList( ZTypedList::buildType( $listType ), $items );
321        }
322
323        if ( !is_object( $object ) ) {
324            self::getLogger()->info(
325                __METHOD__ . ': Input is not a valid ZObject (type: {type})',
326                [ 'type' => gettype( $object ) ]
327            );
328            throw new ZErrorException(
329                ZErrorFactory::createZErrorInstance(
330                    ZErrorTypeRegistry::Z_ERROR_INVALID_FORMAT,
331                    [
332                        'data' => $object
333                    ]
334                )
335            );
336        }
337
338        $typeRegistry = ZTypeRegistry::singleton();
339        $typeZObject = self::extractObjectType( $object );
340        $typeZid = $typeZObject->getZValue();
341        $objectVars = get_object_vars( $object );
342
343        // If typeZObject is a ZReference and a built-in, build args and call constructor
344        if ( ( $typeZObject instanceof ZReference ) && ( $typeRegistry->isZTypeBuiltIn( $typeZid ) ) ) {
345            $typeName = $typeRegistry->getZObjectTypeFromKey( $typeZid );
346            $typeClass = "MediaWiki\\Extension\\WikiLambda\\ZObjects\\$typeName";
347            $objectArgs = self::createKeyValues( $objectVars, $typeClass );
348            // Magic:
349            return new $typeClass( ...$objectArgs );
350        }
351
352        // If typeZObject is a ZFunctionCall and a built-in, build args and call constructor
353        if ( ( $typeZObject instanceof ZFunctionCall ) && ( $typeRegistry->isZFunctionBuiltIn( $typeZid ) ) ) {
354            $builtinName = $typeRegistry->getZFunctionBuiltInName( $typeZid );
355            $builtinClass = "MediaWiki\\Extension\\WikiLambda\\ZObjects\\$builtinName";
356            $objectArgs = self::createKeyValues( $objectVars, $builtinClass );
357            // Magic:
358            return new $builtinClass( $typeZObject, ...$objectArgs );
359        }
360
361        // No built-in type or function call, so we build a generic ZObject instance:
362        // * typeZid is a user-defined type Zid or function Zid, so:
363        // * we check that it exists in the wiki
364        // * we call the ZObject constructor
365        $targetTitle = Title::newFromText( $typeZid, NS_MAIN );
366        if ( !$targetTitle->exists() ) {
367            self::getLogger()->info(
368                __METHOD__ . ': User-defined type {typeZid} not found on wiki',
369                [ 'typeZid' => $typeZid ]
370            );
371            throw new ZErrorException(
372                ZErrorFactory::createZErrorInstance(
373                    ZErrorTypeRegistry::Z_ERROR_ZID_NOT_FOUND,
374                    [
375                        'data' => $typeZid
376                    ]
377                )
378            );
379        }
380        $zObjectStore = WikiLambdaServices::getZObjectStore();
381        $targetObject = $zObjectStore->fetchZObjectByTitle( $targetTitle );
382        if ( !$targetObject ) {
383            self::getLogger()->warning(
384                __METHOD__ . ': User-defined type {typeZid} found but could not be fetched from store',
385                [ 'typeZid' => $typeZid ]
386            );
387            throw new ZErrorException(
388                ZErrorFactory::createZErrorInstance(
389                    ZErrorTypeRegistry::Z_ERROR_ZID_NOT_FOUND,
390                    [
391                        'data' => $typeZid
392                    ]
393                )
394            );
395        }
396
397        $objectArgs = self::createKeyValues( $objectVars, ZObject::class );
398        return new ZObject( ...$objectArgs );
399    }
400
401    /**
402     * This method takes an input and a built-in ZObject type name and returns the
403     * required arguments to call the ZObject constructur
404     *
405     * @param array $objectVars
406     * @param class-string $targetClass
407     * @return array arguments to pass to the target ZObject constructor
408     * @phan-return non-empty-array
409     * @throws ZErrorException
410     */
411    private static function createKeyValues( array $objectVars, string $targetClass ) {
412        $targetDefinition = $targetClass::getDefinition();
413
414        $creationArray = [];
415        foreach ( $targetDefinition['keys'] as $key => $settings ) {
416            if ( array_key_exists( $key, $objectVars ) ) {
417                if ( in_array( $key, ZTypeRegistry::TERMINAL_KEYS ) ) {
418                    // Return the value if it belongs to a terminal key (Z6K1 or Z9K1)
419                    $creationArray[] = $objectVars[ $key ];
420                } else {
421                    // Build the value of a given key to create nested ZObjects
422                    // If it fails, throw a Z526/Key value error
423                    try {
424                        $creationArray[] = self::create( $objectVars[ $key ] );
425                    } catch ( ZErrorException $e ) {
426                        self::getLogger()->info(
427                            __METHOD__ . ': Key value {key} broken: {message}',
428                            [ 'key' => $key, 'message' => $e->getMessage() ]
429                        );
430                        throw new ZErrorException( ZErrorFactory::createKeyValueZError( $key, $e->getZError() ) );
431                    }
432                }
433
434                // Remove every definition key from the objectVars, so that we can pass
435                // them as additional parameters if the definition specifies so.
436                unset( $objectVars[ $key ] );
437
438            } else {
439                // If it doesn't exist in $objectVars, we pass null
440                $creationArray[] = null;
441                if ( array_key_exists( 'required', $settings ) && ( $settings['required'] ) ) {
442                    // Error Z511/Missing key
443                    self::getLogger()->info(
444                        __METHOD__ . ': Required key {key} missing',
445                        [ 'key' => $key ]
446                    );
447                    throw new ZErrorException(
448                        ZErrorFactory::createZErrorInstance(
449                            ZErrorTypeRegistry::Z_ERROR_MISSING_KEY,
450                            [
451                                'data' => $objectVars,
452                                'keywordArgs' => [ 'missing' => $key ]
453                            ]
454                        )
455                    );
456                }
457            }
458        }
459
460        // If ZObject Definition include the parameter 'additionalKeys',
461        // pass the leftover objectVars as the last argument (ignore Z1K1)
462        if ( array_key_exists( 'additionalKeys', $targetDefinition ) && $targetDefinition[ 'additionalKeys' ] ) {
463            $args = [];
464            foreach ( $objectVars as $key => $value ) {
465                if ( $key === ZTypeRegistry::Z_OBJECT_TYPE ) {
466                    continue;
467                }
468                try {
469                    $args[ $key ] = self::create( $value );
470                } catch ( ZErrorException $e ) {
471                    self::getLogger()->info(
472                        __METHOD__ . ': Additional key {key} broken: {message}',
473                        [ 'key' => $key, 'message' => $e->getMessage() ]
474                    );
475                    throw new ZErrorException( ZErrorFactory::createKeyValueZError( $key, $e->getZError() ) );
476                }
477            }
478            $creationArray[] = $args;
479        }
480
481        return $creationArray;
482    }
483
484    /**
485     * Returns the inner ZObject of a given ZPersistentObject representation, which
486     * corresponds to is value key (Z2K2)
487     *
488     * @param \stdClass $object
489     * @return \stdClass|array|string
490     * @throws ZErrorException
491     */
492    private static function extractInnerObject( $object ) {
493        if ( !property_exists( $object, ZTypeRegistry::Z_PERSISTENTOBJECT_VALUE ) ) {
494            self::getLogger()->info( __METHOD__ . ': Missing Z_PERSISTENTOBJECT_VALUE key' );
495            throw new ZErrorException(
496                ZErrorFactory::createZErrorInstance(
497                    ZErrorTypeRegistry::Z_ERROR_MISSING_KEY,
498                    [
499                        'data' => $object,
500                        'keywordArgs' => [ 'missing' => ZTypeRegistry::Z_PERSISTENTOBJECT_VALUE ]
501                    ]
502                )
503            );
504        }
505        return $object->{ ZTypeRegistry::Z_PERSISTENTOBJECT_VALUE };
506    }
507
508    /**
509     * Given a ZObject, return its type.
510     *
511     * It accepts both a canonical or a normal input.
512     *
513     * Throws errors if:
514     * * The object doesn't have a type/Z1K1
515     * * The object type is not a reference or a function call
516     * * The object type reference points at an unexisting object
517     * * The object type reference points at an object that's not a type
518     *
519     * @param \stdClass|array|string $object
520     * @return ZReference|ZFunctionCall Object type represented by a reference or a function call
521     * @throws ZErrorException
522     */
523    private static function extractObjectType( $object ) {
524        // Check for canonical strings and references
525        if ( is_string( $object ) ) {
526            if ( ZObjectUtils::isValidOrNullZObjectReference( $object ) ) {
527                // returns ZReference
528                return new ZReference( ZTypeRegistry::Z_REFERENCE );
529            }
530            // returns ZReference
531            return new ZReference( ZTypeRegistry::Z_STRING );
532        }
533
534        // Check for canonical arrays
535        if ( is_array( $object ) ) {
536            // TODO (T298126): We should probably infer the type of ZObjects contained in
537            // this array instead of just creating an untyped list of Z1s
538            return new ZFunctionCall(
539                new ZReference( ZTypeRegistry::Z_FUNCTION_TYPED_LIST ),
540                [ ZTypeRegistry::Z_FUNCTION_TYPED_LIST_TYPE => ZTypeRegistry::Z_OBJECT_TYPE ]
541            );
542        }
543
544        // Error invalid type
545        if ( !is_object( $object ) ) {
546            self::getLogger()->info(
547                __METHOD__ . ': Invalid type (got {type})',
548                [ 'type' => gettype( $object ) ]
549            );
550            throw new ZErrorException(
551                ZErrorFactory::createZErrorInstance(
552                    ZErrorTypeRegistry::Z_ERROR_INVALID_FORMAT,
553                    [
554                        'data' => $object
555                    ]
556                )
557            );
558        }
559
560        // Error key Z1K1 does not exist
561        if ( !property_exists( $object, ZTypeRegistry::Z_OBJECT_TYPE ) ) {
562            self::getLogger()->info( __METHOD__ . ': Input missing Z1K1 key' );
563            throw new ZErrorException(
564                ZErrorFactory::createZErrorInstance(
565                    ZErrorTypeRegistry::Z_ERROR_MISSING_TYPE,
566                    [
567                        'data' => $object
568                    ]
569                )
570            );
571        }
572
573        // Value of Z1K1 can be a string or an object,
574        // resulting on a ZReference or a ZFunctionCall
575        $objectType = $object->{ ZTypeRegistry::Z_OBJECT_TYPE };
576        $type = self::create( $objectType );
577        $typeRegistry = ZTypeRegistry::singleton();
578
579        // If it's a ZReference, it must point at an object of type Z4
580        if ( $type instanceof ZReference ) {
581            $errorRegistry = ZErrorTypeRegistry::singleton();
582            $typeZid = $type->getZValue();
583
584            // Make sure that the reference is to a Z4
585
586            // TODO (T375065): isZObjectKeyKnown fetches and validates types,
587            // this has the potential of going into infinite loops when creating
588            // error objects.
589            // We should have a more efficient way to check that a Zid belongs to
590            // a type, or a function (the most important cases for creation and
591            // function call, respectively), probably through secondaty tables.
592            // Notice that current labels table has this info but ONLY for those
593            // objects containing labels, which means that:
594            // * the process would not be fully trustworthy, or
595            // * we'd need to make sure that types and functions are stored with labels
596            if ( !$typeRegistry->isZObjectKeyKnown( $typeZid ) ) {
597                self::getLogger()->info(
598                    __METHOD__ . ': Reference type unknown: {typeZid}',
599                    [ 'typeZid' => $typeZid ]
600                );
601                throw new ZErrorException(
602                    ZErrorFactory::createZErrorInstance(
603                        ZErrorTypeRegistry::Z_ERROR_UNKNOWN_REFERENCE,
604                        [
605                            'data' => $typeZid
606                        ]
607                    )
608                );
609            }
610
611            // return ZReference
612            return $type;
613        }
614
615        if ( $type instanceof ZFunctionCall ) {
616            // Only check return type for non built-in type functions, as we know
617            // that those have Z4 as their return type
618            if ( !$typeRegistry->isZFunctionBuiltIn( $type->getZValue() ) ) {
619                $returnType = $type->getReturnType();
620                // Make sure that the function Zid exists
621                if ( $returnType === null ) {
622                    self::getLogger()->info(
623                        __METHOD__ . ': Function call return type for {zValue} not found',
624                        [ 'zValue' => $type->getZValue() ]
625                    );
626                    throw new ZErrorException(
627                        ZErrorFactory::createZErrorInstance(
628                            ZErrorTypeRegistry::Z_ERROR_ZID_NOT_FOUND,
629                            [
630                                'data' => $type->getZValue()
631                            ]
632                        )
633                    );
634                }
635                // Make sure that the return type of the function is Z4 or Z1
636                if (
637                    ( $returnType !== ZTypeRegistry::Z_TYPE ) &&
638                    ( $returnType !== ZTypeRegistry::Z_OBJECT )
639                ) {
640                    self::getLogger()->info(
641                        __METHOD__ . ': Function call return type is wrong: {returnType}',
642                        [ 'returnType' => $type->getReturnType() ]
643                    );
644                    throw new ZErrorException(
645                        ZErrorFactory::createZErrorInstance(
646                            ZErrorTypeRegistry::Z_ERROR_UNEXPECTED_ZTYPE,
647                            [
648                                'expected' => ZTypeRegistry::Z_TYPE,
649                                'used' => $type->getReturnType()
650                            ]
651                        )
652                    );
653                }
654            }
655            // return ZFunctionCall
656            return $type;
657        }
658
659        if ( $type instanceof ZType ) {
660            // return the content of the Type Identity field
661            return $type->getTypeId();
662        }
663
664        // Invalid type: Z1K1 contains something else than a ZReference or a ZFunctionCall
665        self::getLogger()->info(
666            __METHOD__ . ': Z1K1 is not a ZReference or ZFunctionCall (class: {class})',
667            [ 'class' => get_class( $type ) ]
668        );
669        throw new ZErrorException(
670            ZErrorFactory::createZErrorInstance(
671                ZErrorTypeRegistry::Z_ERROR_REFERENCE_VALUE_INVALID,
672                [
673                    'data' => $type->getZValue()
674                ]
675            )
676        );
677    }
678
679    /**
680     * @const bool
681     */
682    private const SET_SELF_ZID = 1;
683    private const UNSET_SELF_ZID = 2;
684    private const CHECK_SELF_ZID = 3;
685
686    /**
687     * Tracks Zids that appear in the ZObject validation context, which might referenced again from
688     * another key of the same ZObject. Depending on the mode flag, it sets a newly observed Zid,
689     * unsets it or just checks its presence.
690     *
691     * @param string $zid
692     * @param int $mode
693     * @return bool
694     */
695    private static function trackSelfReference( $zid, $mode = self::CHECK_SELF_ZID ): bool {
696        static $context = [];
697        $isObserved = array_key_exists( $zid, $context );
698
699        switch ( $mode ) {
700            case self::CHECK_SELF_ZID:
701                return $isObserved;
702            case self::SET_SELF_ZID:
703                $context[ $zid ] = true;
704                return $isObserved;
705            case self::UNSET_SELF_ZID:
706                unset( $context[ $zid ] );
707                return $isObserved;
708            default:
709                return false;
710        }
711    }
712}