wikimedia/mediawiki-extensions-Wikibase

View on GitHub
lib/includes/DataTypeDefinitions.php

Summary

Maintainability
A
2 hrs
Test Coverage
<?php

namespace Wikibase\Lib;

use UnexpectedValueException;
use Wikibase\DataModel\Deserializers\SnakValueDeserializer;
use Wikimedia\Assert\Assert;

/**
 * Service that manages property data type definition. This is a registry that provides access to
 * factory functions for various services associated with property data types, such as validators,
 * parsers, and formatters.
 *
 * DataTypeDefinitions provides a one-stop interface for defining property data types.
 * Each property data type is defined using a "data type definition" array.
 * A definition array has the following fields:
 * - value-type: the value type used with the data type
 * - validator-factory-callback: a callback for creating validators for the data type,
 *   as used by BuilderBasedDataTypeValidatorFactory.
 * - parser-factory-callback: a callback for instantiating a parser for the data type
 * - deserializer-builder: a callback or DataValue class string for deserializing the value
 * - formatter-factory-callback: a callback for instantiating a formatter for the data type
 * - rdf-builder-factory-callback: a callback for instantiating a rdf mapping for the data type
 *
 * DataTypeDefinitions also supports fallback logic based on the value type associated with each
 * property data type.
 *
 * @see docs/datatypes.wiki
 *
 * @license GPL-2.0-or-later
 * @author Daniel Kinzler
 */
class DataTypeDefinitions {

    /**
     * Constant for indicating that callback maps should be returned with the "VT:" and "PT:"
     * prefixes in the array keys indicating whether the callback applies to a value type or a
     * property data type.
     */
    public const PREFIXED_MODE = 'prefixed';

    /**
     * Constant for indicating that callback maps should be returned for property data types only,
     * with no prefixes in the array keys, but with fallbacks for value types merged into the
     * definitions for the property data types.
     */
    public const RESOLVED_MODE = 'resolved';

    private const VALUE_TYPE_PREFIX = 'VT:';
    private const DATA_TYPE_PREFIX = 'PT:';

    /**
     * @var array[]
     */
    private $dataTypeDefinitions = [];

    /**
     * @param array[] $dataTypeDefinitions An associative array mapping property data type ids
     * (with the prefix "PT:") and value types (with the prefix "VT:") to data type definitions.
     * Each data type definitions are associative arrays, refer to the class level documentation
     * for details.
     * @param string[] $disabledDataTypes Array of disabled data types.
     */
    public function __construct( array $dataTypeDefinitions, array $disabledDataTypes = [] ) {
        $dataTypeDefinitions = $this->filterDisabledDataTypes(
            $dataTypeDefinitions,
            $disabledDataTypes
        );

        $this->registerDataTypes( $dataTypeDefinitions );
    }

    /**
     * Adds data type definitions. The new definitions are merged with the existing definitions.
     * If a data type in $dataTypeDefinitions was already defined, the old definition is not
     * replaced but the definitions are merged.
     *
     * @param array[] $dataTypeDefinitions An associative array mapping property data type ids
     * (with the prefix "PT:") and value types (with the prefix "VT:") to data type definitions.
     * Each data type definitions are associative arrays, refer to the class level documentation
     * for details.
     */
    public function registerDataTypes( array $dataTypeDefinitions ) {
        Assert::parameterElementType( 'array', $dataTypeDefinitions, '$dataTypeDefinitions' );

        foreach ( $dataTypeDefinitions as $id => $def ) {
            Assert::parameter(
                strpos( $id, ':' ),
                "\$dataTypeDefinitions[$id]",
                'Key must start with a prefix like "PT:" or "VT:".'
            );

            if ( isset( $this->dataTypeDefinitions[$id] ) ) {
                $this->dataTypeDefinitions[$id] = array_merge(
                    $this->dataTypeDefinitions[$id],
                    $dataTypeDefinitions[$id]
                );
            } else {
                $this->dataTypeDefinitions[$id] = $dataTypeDefinitions[$id];
            }
        }
    }

    /**
     * @param array[] $dataTypeDefinitions Associative array of data types and definitions.
     * @param string[] $disabledTypes List of disabled data types
     *
     * @return array[] Filtered data type definitions
     */
    private function filterDisabledDataTypes( array $dataTypeDefinitions, array $disabledTypes ) {
        foreach ( $dataTypeDefinitions as $id => $def ) {
            if ( strpos( $id, 'PT' ) === 0 ) {
                if ( in_array( substr( $id, 3 ), $disabledTypes ) ) {
                    unset( $dataTypeDefinitions[$id] );
                }
            }
        }

        return $dataTypeDefinitions;
    }

    /**
     * @param array $map
     * @param string $prefix
     *
     * @return array A filtered version of $map that only contains the entries
     *         with keys that match the prefix $prefix, with that prefix removed.
     */
    private function getFilteredByPrefix( array $map, $prefix ) {
        $filtered = [];

        foreach ( $map as $key => $value ) {
            $ofs = strlen( $prefix );
            if ( strpos( $key, $prefix ) === 0 ) {
                $key = substr( $key, $ofs );
                $filtered[$key] = $value;
            }
        }

        return $filtered;
    }

    /**
     * @return string[] a list of all registered property data types.
     */
    public function getTypeIds() {
        $ptDefinitions = $this->getFilteredByPrefix( $this->dataTypeDefinitions, self::DATA_TYPE_PREFIX );
        return array_keys( $ptDefinitions );
    }

    public function getExpertModules() {
        return $this->resolveValueTypeFallback(
            $this->getMapForDefinitionField( 'expert-module' )
        );
    }

    /**
     * @param string $field
     *
     * @return array An associative array mapping type IDs (with "VT:" or "PT:" prefixes) to the
     * value of $field given in the original property data type definition provided to the
     * constructor.
     */
    private function getMapForDefinitionField( $field ) {
        $fieldValues = [];

        foreach ( $this->dataTypeDefinitions as $id => $def ) {
            if ( isset( $def[$field] ) ) {
                $fieldValues[$id] = $def[$field];
            }
        }

        return $fieldValues;
    }

    /**
     * Resolves value type fallbacks on the given definition map. For each property data type,
     * the corresponding value type is determined. Then, any data type missing from $definitions
     * is filled in with the value for the corresponding value type. The resulting array will
     * have no PT or VT prefixes.
     *
     * @param array $definitions The map to process.
     * @param bool $allowMissing Whether to allow missing definitions,
     * i.e. known data types with no definition entry matching their data type or value type.
     * By default, this throws an exception, so that if the function returns successfully,
     * the resulting array is guaranteed to contain all data types.
     *
     * @throws UnexpectedValueException
     * @return array An associative array mapping data type IDs to one of the $definitions values.
     * The keys in this array are plain property data type IDs without a prefix.
     */
    private function resolveValueTypeFallback( array $definitions, bool $allowMissing = false ) {
        $resolved = [];

        foreach ( $this->getValueTypes() as $propertyType => $valueType ) {
            $ptKey = "PT:$propertyType";
            $vtKey = "VT:$valueType";

            if ( !empty( $definitions[$ptKey] ) ) {
                $resolved[$propertyType] = $definitions[$ptKey];
            } elseif ( !empty( $definitions[$vtKey] ) ) {
                $resolved[$propertyType] = $definitions[$vtKey];
            } elseif ( !$allowMissing ) {
                throw new UnexpectedValueException( "Missing definition for $ptKey or $vtKey" );
            }
        }

        return $resolved;
    }

    /**
     * Applies the given mode to the $callbackMap. If $mode is PREFIXED_MODE, $callbackMap is
     * returned unchanged. If $mode is RESOLVED_MODE, resolveValueTypeFallback() is applied
     * to $callbackMap. The resulting map will have no prefixes in the array keys, and will
     * contain entries for all property data types, with value type fallback applied.
     *
     * @param array $callbackMap
     * @param string $mode PREFIXED_MODE or RESOLVED_MODE
     * @param bool $allowMissing See {@link self::resolveValueTypeFallback()}
     *
     * @return array A version of $callbackMap with $mode applied.
     */
    private function applyMode( array $callbackMap, string $mode, bool $allowMissing = false ) {
        if ( $mode === self::RESOLVED_MODE ) {
            return $this->resolveValueTypeFallback( $callbackMap, $allowMissing );
        } else {
            return $callbackMap;
        }
    }

    /**
     * @return string[] An associative array mapping property data types to value types.
     */
    public function getValueTypes() {
        return $this->getFilteredByPrefix(
            $this->getMapForDefinitionField( 'value-type' ),
            self::DATA_TYPE_PREFIX
        );
    }

    /**
     * @return string[] An associative array mapping some property data types to types URIs for
     *         use in RDF. This does not have to cover all known property data types. For those
     *         that od not explicitly define a URI, RdfVocabulary will generate one.
     *         Note that property data type URIs are not intended to be used as RDF literal types.
     */
    public function getRdfTypeUris() {
        return $this->getFilteredByPrefix(
            $this->getMapForDefinitionField( 'rdf-uri' ),
            self::DATA_TYPE_PREFIX
        );
    }

    /**
     * @see BuilderBasedDataTypeValidatorFactory
     *
     * @param string $mode PREFIXED_MODE to request a callback map with "VT:" and "PT:" prefixes
     * for value types and property data types, or RESOLVED_MODE to retrieve a callback map for
     * property data types only, with value type fallback applied.
     *
     * @return callable[]
     */
    public function getValidatorFactoryCallbacks( $mode = self::RESOLVED_MODE ) {
        return $this->applyMode(
            $this->getMapForDefinitionField( 'validator-factory-callback' ),
            $mode
        );
    }

    /**
     * @see ValueParserFactory
     *
     * @param string $mode PREFIXED_MODE to request a callback map with "VT:" and "PT:" prefixes
     * for value types and property data types, or RESOLVED_MODE to retrieve a callback map for
     * property data types only, with value type fallback applied.
     *
     * @return callable[]
     */
    public function getParserFactoryCallbacks( $mode = self::RESOLVED_MODE ) {
        return $this->applyMode(
            $this->getMapForDefinitionField( 'parser-factory-callback' ),
            $mode
        );
    }

    /**
     * @see SnakValueDeserializer
     *
     * @param string $mode PREFIXED_MODE to request a callback map with "VT:" and "PT:" prefixes
     * for value types and property data types, or RESOLVED_MODE to retrieve a builder map for
     * property data types only, with value type fallback applied.
     *
     * @return array<callable|string>
     */
    public function getDeserializerBuilders( $mode = self::RESOLVED_MODE ) {
        return $this->applyMode(
            $this->getMapForDefinitionField( 'deserializer-builder' ),
            $mode
        );
    }

    /**
     * @return array<callable|string> map of value type IDs to deserializer builder callbacks or DataValue class names
     */
    public function getDataValueDeserializerBuilders(): array {
        $allBuilders = $this->getMapForDefinitionField( 'deserializer-builder' );
        $dataValueBuilders = [];
        foreach ( $allBuilders as $type => $builder ) {
            if ( str_starts_with( $type, self::VALUE_TYPE_PREFIX ) ) {
                $dataValueBuilders[substr( $type, strlen( self::VALUE_TYPE_PREFIX ) )] = $builder;
            }
        }

        return $dataValueBuilders;
    }

    /**
     * @see OutputFormatValueFormatterFactory
     *
     * @param string $mode PREFIXED_MODE to request a callback map with "VT:" and "PT:" prefixes
     * for value types and property data types, or RESOLVED_MODE to retrieve a callback map for
     * property data types only, with value type fallback applied.
     *
     * @return callable[]
     */
    public function getFormatterFactoryCallbacks( $mode = self::RESOLVED_MODE ) {
        return $this->applyMode(
            $this->getMapForDefinitionField( 'formatter-factory-callback' ),
            $mode
        );
    }

    /**
     * @see OutputFormatSnakFormatterFactory
     *
     * @return callable[]
     */
    public function getSnakFormatterFactoryCallbacks() {
        return $this->getMapForDefinitionField( 'snak-formatter-factory-callback' );
    }

    /**
     * @see ValueSnakRdfBuilderFactory
     *
     * @param string $mode PREFIXED_MODE to request a callback map with "VT:" and "PT:" prefixes
     * for value types and property data types, or RESOLVED_MODE to retrieve a callback map for
     * property data types only, with value type fallback applied.
     *
     * @return callable[]
     */
    public function getRdfBuilderFactoryCallbacks( $mode = self::RESOLVED_MODE ) {
        return $this->applyMode(
            $this->getMapForDefinitionField( 'rdf-builder-factory-callback' ),
            $mode
        );
    }

    /**
     * Get data formatters for search indexing for each type.
     *
     * @param string $mode PREFIXED_MODE to request a callback map with "VT:" and "PT:" prefixes
     * for value types and property data types, or RESOLVED_MODE to retrieve a callback map for
     * property data types only, with value type fallback applied.
     *
     * @return callable[] A partial map with keys depending on the $mode.
     * Note that, unlike in other methods of this class,
     * the returned map is not guaranteed to cover all data or value types:
     * there are several data types and value types with no defined search index data formatter.
     */
    public function getSearchIndexDataFormatterCallbacks( $mode = self::RESOLVED_MODE ) {
        return $this->applyMode(
            $this->getMapForDefinitionField( 'search-index-data-formatter-callback' ),
            $mode,
            true
        );
    }

    /**
     * Produce array of types for RDF.
     * Using PropertyRdfBuilder constants for data types is recommended.
     * In situations where PropertyRdfBuilder has not been autoloaded yet,
     * the type may be wrapped in a callback.
     * @return string[]
     */
    public function getRdfDataTypes() {
        return array_map(
            function ( $dataType ) {
                if ( is_callable( $dataType ) ) {
                    $dataType = ( $dataType )();
                }
                return $dataType;
            },
            $this->getFilteredByPrefix(
                $this->getMapForDefinitionField( 'rdf-data-type' ),
                self::DATA_TYPE_PREFIX
            )
        );
    }

    /**
     * Get {@link DataValueNormalizer data value normalizers} for each type.
     * @return callable[] List of callbacks, with keys having "VT:" and "PT:" prefixes.
     */
    public function getNormalizerFactoryCallbacks(): array {
        return $this->getMapForDefinitionField( 'normalizer-factory-callback' );
    }

}