wikimedia/mediawiki-extensions-Wikibase

View on GitHub
client/includes/DataAccess/Scribunto/EntityAccessor.php

Summary

Maintainability
A
1 hr
Test Coverage
<?php

namespace Wikibase\Client\DataAccess\Scribunto;

use InvalidArgumentException;
use MediaWiki\Language\Language;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use Serializers\Serializer;
use Wikibase\Client\Serializer\ClientEntitySerializer;
use Wikibase\Client\Serializer\ClientStatementListSerializer;
use Wikibase\Client\Usage\UsageAccumulator;
use Wikibase\DataModel\Entity\EntityIdParser;
use Wikibase\DataModel\Entity\NumericPropertyId;
use Wikibase\DataModel\Services\Lookup\EntityLookup;
use Wikibase\DataModel\Services\Lookup\PropertyDataTypeLookup;
use Wikibase\DataModel\Services\Lookup\UnresolvedEntityRedirectException;
use Wikibase\DataModel\Statement\StatementListProvider;
use Wikibase\Lib\ContentLanguages;
use Wikibase\Lib\TermLanguageFallbackChain;

/**
 * Functionality needed to expose Entities to Lua.
 *
 * @license GPL-2.0-or-later
 */
class EntityAccessor {

    /**
     * @var EntityIdParser
     */
    private $entityIdParser;

    /**
     * @var EntityLookup
     */
    private $entityLookup;

    /**
     * @var UsageAccumulator
     */
    private $usageAccumulator;

    /**
     * @var Serializer
     */
    private $entitySerializer;

    /**
     * @var Serializer
     */
    private $statementSerializer;

    /**
     * @var PropertyDataTypeLookup
     */
    private $dataTypeLookup;

    /**
     * @var TermLanguageFallbackChain
     */
    private $termFallbackChain;

    /**
     * @var Language
     */
    private $language;

    /**
     * @var ContentLanguages
     */
    private $termsLanguages;

    /**
     * @var LoggerInterface
     */
    private $logger;

    /**
     * @param EntityIdParser $entityIdParser
     * @param EntityLookup $entityLookup
     * @param UsageAccumulator $usageAccumulator
     * @param Serializer $entitySerializer
     * @param Serializer $statementSerializer
     * @param PropertyDataTypeLookup $dataTypeLookup
     * @param TermLanguageFallbackChain $termFallbackChain
     * @param Language $language
     * @param ContentLanguages $termsLanguages
     */
    public function __construct(
        EntityIdParser $entityIdParser,
        EntityLookup $entityLookup,
        UsageAccumulator $usageAccumulator,
        Serializer $entitySerializer,
        Serializer $statementSerializer,
        PropertyDataTypeLookup $dataTypeLookup,
        TermLanguageFallbackChain $termFallbackChain,
        Language $language,
        ContentLanguages $termsLanguages,
        LoggerInterface $logger = null
    ) {
        $this->entityIdParser = $entityIdParser;
        $this->entityLookup = $entityLookup;
        $this->usageAccumulator = $usageAccumulator;
        $this->entitySerializer = $entitySerializer;
        $this->statementSerializer = $statementSerializer;
        $this->dataTypeLookup = $dataTypeLookup;
        $this->termFallbackChain = $termFallbackChain;
        $this->language = $language;
        $this->termsLanguages = $termsLanguages;
        $this->logger = $logger ?: new NullLogger();
    }

    /**
     * Recursively renumber a serialized array in place, so it is indexed at 1, not 0.
     * Just like Lua wants it.
     *
     * @param array &$entityArr
     */
    private function renumber( array &$entityArr ) {
        foreach ( $entityArr as &$value ) {
            if ( !is_array( $value ) ) {
                continue;
            }
            if ( isset( $value[0] ) ) {
                array_unshift( $value, null );
                unset( $value[0] );
            }
            $this->renumber( $value );
        }
    }

    /**
     * Get entity from prefixed ID (e.g. "Q23") and return it as serialized array.
     *
     * @param string $prefixedEntityId
     *
     * @return array|null
     */
    public function getEntity( $prefixedEntityId ) {
        $prefixedEntityId = trim( $prefixedEntityId );

        $entityId = $this->entityIdParser->parse( $prefixedEntityId );

        try {
            $entityObject = $this->entityLookup->getEntity( $entityId );
        } catch ( UnresolvedEntityRedirectException $ex ) {
            $this->logPossibleDoubleRedirect( $prefixedEntityId );

            return null;
        }

        if ( $entityObject === null ) {
            return null;
        }

        $entityArr = $this->newClientEntitySerializer()->serialize( $entityObject );

        // Renumber the entity as Lua uses 1-based array indexing
        $this->renumber( $entityArr );
        $entityArr['schemaVersion'] = 2;

        return $entityArr;
    }

    /**
     * Find out whether an entity exists.
     *
     * @param string $prefixedEntityId
     *
     * @return bool
     */
    public function entityExists( $prefixedEntityId ) {
        $prefixedEntityId = trim( $prefixedEntityId );

        $entityId = $this->entityIdParser->parse( $prefixedEntityId );

        // This doesn't really depend on any aspect of the entity specifically.
        $this->usageAccumulator->addOtherUsage( $entityId );
        try {
            return $this->entityLookup->hasEntity( $entityId );
        } catch ( UnresolvedEntityRedirectException $ex ) {
            $this->logPossibleDoubleRedirect( $prefixedEntityId );

            return false;
        }
    }

    /**
     * Get statement list from prefixed ID (e.g. "Q23") and property (e.g "P123") and return it as serialized array.
     *
     * @param string $prefixedEntityId
     * @param string $propertyIdSerialization
     * @param string $rank Which statements to include. Either "best" or "all".
     *
     * @return array|null
     */
    public function getEntityStatements( $prefixedEntityId, $propertyIdSerialization, $rank ) {
        $prefixedEntityId = trim( $prefixedEntityId );
        $entityId = $this->entityIdParser->parse( $prefixedEntityId );

        $propertyId = new NumericPropertyId( $propertyIdSerialization );
        $this->usageAccumulator->addStatementUsage( $entityId, $propertyId );

        try {
            $entity = $this->entityLookup->getEntity( $entityId );
        } catch ( UnresolvedEntityRedirectException $ex ) {
            $this->logPossibleDoubleRedirect( $prefixedEntityId );

            return null;
        }

        if ( !( $entity instanceof StatementListProvider ) ) {
            return null;
        }

        $statements = $entity->getStatements()->getByPropertyId( $propertyId );

        if ( $rank === 'best' ) {
            $statements = $statements->getBestStatements();
        } elseif ( $rank !== 'all' ) {
            throw new InvalidArgumentException( '$rank must be "best" or "all", "' . $rank . '" given' );
        }

        $serialization = $this->newClientStatementListSerializer()->serialize( $statements );
        $this->renumber( $serialization );
        return $serialization;
    }

    private function newClientEntitySerializer() {
        return new ClientEntitySerializer(
            $this->entitySerializer,
            $this->dataTypeLookup,
            $this->entityIdParser,
            array_unique( array_merge(
                $this->termsLanguages->getLanguages(),
                $this->termFallbackChain->getFetchLanguageCodes(),
                [ $this->language->getCode() ]
            ) ),
            [ $this->language->getCode() => $this->termFallbackChain ]
        );
    }

    private function newClientStatementListSerializer() {
        return new ClientStatementListSerializer(
            $this->statementSerializer,
            $this->dataTypeLookup,
            $this->entityIdParser
        );
    }

    /**
     * @see UnresolvedEntityRedirectException
     * @param string $prefixedEntityId
     */
    private function logPossibleDoubleRedirect( $prefixedEntityId ) {
        $this->logger->info( 'Unresolved redirect encountered loading {prefixedEntityId}. This is typically cleaned up asynchronously.',
            [
                'prefixedEntityId' => $prefixedEntityId,
            ]
        );
    }

}