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