wikimedia/mediawiki-extensions-Wikibase

View on GitHub
client/includes/Usage/Sql/SqlSubscriptionManager.php

Summary

Maintainability
A
2 hrs
Test Coverage
<?php

declare( strict_types = 1 );

namespace Wikibase\Client\Usage\Sql;

use Exception;
use InvalidArgumentException;
use Wikibase\Client\Usage\SubscriptionManager;
use Wikibase\DataModel\Entity\EntityId;
use Wikimedia\Rdbms\IDatabase;
use Wikimedia\Rdbms\IReadableDatabase;
use Wikimedia\Rdbms\SessionConsistentConnectionManager;

/**
 * SubscriptionManager implementation backed by an SQL table.
 *
 * @see docs/usagetracking.wiki
 *
 * @license GPL-2.0-or-later
 * @author Daniel Kinzler
 */
class SqlSubscriptionManager implements SubscriptionManager {

    /**
     * @var SessionConsistentConnectionManager
     */
    private $connectionManager;

    public function __construct( SessionConsistentConnectionManager $connectionManager ) {
        $this->connectionManager = $connectionManager;
    }

    /**
     * @param EntityId[] $entityIds
     *
     * @return string[]
     */
    private function idsToString( array $entityIds ): array {
        return array_map( function( EntityId $id ) {
            return $id->getSerialization();
        }, $entityIds );
    }

    /**
     * @see SubscriptionManager::subscribe
     *
     * @param string $subscriber
     * @param EntityId[] $entityIds
     *
     * @throws InvalidArgumentException
     * @throws Exception
     */
    public function subscribe( string $subscriber, array $entityIds ): void {
        $subscriptions = $this->idsToString( $entityIds );
        $dbw = $this->connectionManager->getWriteConnection();
        $dbw->startAtomic( __METHOD__ );
        $oldSubscriptions = $this->querySubscriptions( $dbw, $subscriber, $subscriptions );
        $newSubscriptions = array_diff( $subscriptions, $oldSubscriptions );
        $this->insertSubscriptions( $dbw, $subscriber, $newSubscriptions );
        $dbw->endAtomic( __METHOD__ );
    }

    /**
     * @see SubscriptionManager::unsubscribe
     *
     * @param string $subscriber Global site ID of the client
     * @param EntityId[] $entityIds The entities to subscribe to.
     *
     * @throws InvalidArgumentException
     * @throws Exception
     */
    public function unsubscribe( string $subscriber, array $entityIds ): void {
        $unsubscriptions = $this->idsToString( $entityIds );
        $dbw = $this->connectionManager->getWriteConnection();
        $dbw->startAtomic( __METHOD__ );
        $oldSubscriptions = $this->querySubscriptions( $dbw, $subscriber, $unsubscriptions );
        $obsoleteSubscriptions = array_intersect( $unsubscriptions, $oldSubscriptions );
        $this->deleteSubscriptions( $dbw, $subscriber, $obsoleteSubscriptions );
        $dbw->endAtomic( __METHOD__ );
    }

    /**
     * For a set of potential subscriptions, returns the existing subscriptions.
     *
     * @param IReadableDatabase $db
     * @param string $subscriber
     * @param string[] $subscriptions
     *
     * @return string[] Entity ID strings from $subscriptions which $subscriber is already subscribed to.
     */
    private function querySubscriptions( IReadableDatabase $db, string $subscriber, array $subscriptions ): array {
        if ( $subscriptions ) {
            $subscriptions = $db->newSelectQueryBuilder()
                ->select( 'cs_entity_id' )
                ->from( 'wb_changes_subscription' )
                ->where( [
                    'cs_subscriber_id' => $subscriber,
                    'cs_entity_id' => array_values( $subscriptions ),
                ] )
                ->caller( __METHOD__ )->fetchFieldValues();
        }

        return $subscriptions;
    }

    /**
     * Inserts a set of subscriptions.
     *
     * @param IDatabase $db
     * @param string $subscriber
     * @param string[] $subscriptions
     */
    private function insertSubscriptions( IDatabase $db, string $subscriber, array $subscriptions ): void {
        if ( $subscriptions === [] ) {
            return;
        }

        $rows = $this->makeSubscriptionRows( $subscriber, $subscriptions );

        $db->newInsertQueryBuilder()
            ->insertInto( 'wb_changes_subscription' )
            ->ignore()
            ->rows( $rows )
            ->caller( __METHOD__ )->execute();
    }

    /**
     * Inserts a set of subscriptions.
     *
     * @param IDatabase $db
     * @param string $subscriber
     * @param string[] $subscriptions
     */
    private function deleteSubscriptions( IDatabase $db, string $subscriber, array $subscriptions ): void {
        if ( $subscriptions ) {
            $db->newDeleteQueryBuilder()
                ->deleteFrom( 'wb_changes_subscription' )
                ->where( [
                    'cs_subscriber_id' => $subscriber,
                    'cs_entity_id' => $subscriptions,
                ] )
                ->caller( __METHOD__ )->execute();
        }
    }

    /**
     * Returns a list of rows for insertion, using IDatabase's multi-row insert mechanism.
     * Each row is represented as [ $subscriber, $entityId ].
     *
     * @param string $subscriber
     * @param string[] $subscriptions
     *
     * @return array[] rows
     */
    private function makeSubscriptionRows( string $subscriber, array $subscriptions ): array {
        $rows = [];

        foreach ( $subscriptions as $entityId ) {
            $rows[] = [
                'cs_entity_id' => $entityId,
                'cs_subscriber_id' => $subscriber,
            ];
        }

        return $rows;
    }

}