%PDF- %PDF-
Direktori : /www/varak.net/wiki.varak.net/extensions/CirrusSearch/includes/ |
Current File : /www/varak.net/wiki.varak.net/extensions/CirrusSearch/includes/CirrusSearch.php |
<?php use CirrusSearch\Connection; use CirrusSearch\ElasticsearchIntermediary; use CirrusSearch\InterwikiSearcher; use CirrusSearch\Search\FullTextResultsType; use CirrusSearch\Searcher; use CirrusSearch\CompletionSuggester; use CirrusSearch\Search\ResultSet; use CirrusSearch\SearchConfig; use CirrusSearch\Search\FancyTitleResultsType; use CirrusSearch\Search\TitleResultsType; use CirrusSearch\Search\TextIndexField; use CirrusSearch\UserTesting; use MediaWiki\Logger\LoggerFactory; use MediaWiki\MediaWikiServices; /** * SearchEngine implementation for CirrusSearch. Delegates to * CirrusSearchSearcher for searches and CirrusSearchUpdater for updates. Note * that lots of search behavior is hooked in CirrusSearchHooks rather than * overridden here. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * http://www.gnu.org/copyleft/gpl.html */ class CirrusSearch extends SearchEngine { const MORE_LIKE_THIS_PREFIX = 'morelike:'; const MORE_LIKE_THIS_JUST_WIKIBASE_PREFIX = 'morelikewithwikibase:'; const COMPLETION_SUGGESTER_FEATURE = 'completionSuggester'; /** @const string name of the prefixsearch fallback profile */ const COMPLETION_PREFIX_FALLBACK_PROFILE = 'classic'; /** * @var string The last prefix substituted by replacePrefixes. */ private $lastNamespacePrefix; /** * @var array metrics about the last thing we searched sourced from the * Searcher instance */ private $lastSearchMetrics = []; /** * @var array additional metrics about the search sourced within this class */ private $extraSearchMetrics = []; /** * @var string */ private $indexBaseName; /** * @var Connection */ private $connection; /** * Search configuration. * @var SearchConfig */ private $config; /** * Current request. * @var WebRequest */ private $request; public function __construct( $baseName = null ) { // Initialize UserTesting before we create a Connection // This is useful to do tests accross multiple clusters UserTesting::getInstance(); $this->config = MediaWikiServices::getInstance() ->getConfigFactory() ->makeConfig( 'CirrusSearch' ); $this->indexBaseName = $baseName === null ? $this->config->get( SearchConfig::INDEX_BASE_NAME ) : $baseName; $this->connection = new Connection( $this->config ); $this->request = RequestContext::getMain()->getRequest(); } public function setConnection( Connection $connection ) { $this->connection = $connection; } /** * @return Connection */ public function getConnection() { return $this->connection; } /** * Set search config * @param SearchConfig $config */ public function setConfig( SearchConfig $config ) { $this->config = $config; } /** * Get search config * @return SearchConfig */ public function getConfig() { return $this->config; } /** * Override supports to shut off updates to Cirrus via the SearchEngine infrastructure. Page * updates and additions are chained on the end of the links update job. Deletes are noticed * via the ArticleDeleteComplete hook. * @param string $feature feature name * @return bool is this feature supported? */ public function supports( $feature ) { switch ( $feature ) { case 'search-update': case 'list-redirects': return false; default: return parent::supports( $feature ); } } /** * Overridden to delegate prefix searching to Searcher. * @param string $term text to search * @return ResultSet|null|Status results, no results, or error respectively */ public function searchText( $term ) { $config = null; if ( $this->request && $this->request->getVal( 'cirrusLang' ) ) { $config = new SearchConfig( $this->request->getVal( 'cirrusLang' ) ); } $matches = $this->searchTextReal( $term, $config ); if (!$matches instanceof ResultSet) { return $matches; } if ( $this->isFeatureEnabled( 'rewrite' ) && $matches->isQueryRewriteAllowed( $GLOBALS['wgCirrusSearchInterwikiThreshold'] ) ) { $matches = $this->searchTextSecondTry( $term, $matches ); } ElasticsearchIntermediary::setResultPages( [ $matches ] ); return $matches; } /** * Check whether we want to try another language. * @param string $term Search term * @return string[]|null Array of (interwiki, dbname) for another wiki to try, or null */ private function hasSecondaryLanguage( $term ) { if ( empty( $GLOBALS['wgCirrusSearchLanguageToWikiMap'] ) || empty( $GLOBALS['wgCirrusSearchWikiToNameMap'] ) ) { // map's empty - no need to bother with detection return null; } $detected = null; foreach ( $GLOBALS['wgCirrusSearchLanguageDetectors'] as $name => $klass ) { if ( !class_exists( $klass ) ) { LoggerFactory::getInstance( 'CirrusSearch' )->info( "Unknown detector class for {name}: {class}", [ "name" => $name, "class" => $klass, ] ); continue; } $detector = new $klass(); if( !( $detector instanceof \CirrusSearch\LanguageDetector\Detector ) ) { LoggerFactory::getInstance( 'CirrusSearch' )->info( "Bad detector class for {name}: {class}", [ "name" => $name, "class" => $klass, ] ); continue; } $lang = $detector->detect( $this, $term ); $wiki = self::wikiForLanguage( $lang ); if ( $wiki !== null ) { // it might be more accurate to attach these to the 'next' // log context? It would be inconsistent with the // langdetect => false condition which does not have a next // request though. Searcher::appendLastLogContext( [ 'langdetect' => $name, ] ); $detected = $wiki; break; } } if ( $detected === null ) { Searcher::appendLastLogContext( [ 'langdetect' => 'failed', ] ); } else { // Report language detection with search metrics $this->extraSearchMetrics['wgCirrusSearchAltLanguage'] = $detected; } return $detected; } /** * @param string $lang Language code to find wiki for * @return string[]|null Array of (interwiki, dbname) for wiki related to specified language code */ private static function wikiForLanguage( $lang ) { if ( empty( $GLOBALS['wgCirrusSearchLanguageToWikiMap'][$lang] ) ) { return null; } $interwiki = $GLOBALS['wgCirrusSearchLanguageToWikiMap'][$lang]; if ( empty( $GLOBALS['wgCirrusSearchWikiToNameMap'][$interwiki] ) ) { return null; } $interWikiId = $GLOBALS['wgCirrusSearchWikiToNameMap'][$interwiki]; if ( $interWikiId == wfWikiID() ) { // we're back to the same wiki, no use to try again return null; } return [ $interwiki, $interWikiId ]; } /** * @param string $feature * @return bool */ private function isFeatureEnabled( $feature ) { return isset( $this->features[$feature] ) && $this->features[$feature]; } /** * @param string $term * @param ResultSet $oldResult * @return ResultSet */ private function searchTextSecondTry( $term, ResultSet $oldResult ) { // TODO: figure out who goes first - language or suggestion? if ( $oldResult->numRows() == 0 && $oldResult->hasSuggestion() ) { $rewritten = $oldResult->getSuggestionQuery(); $rewrittenSnippet = $oldResult->getSuggestionSnippet(); $this->showSuggestion = false; $rewrittenResult = $this->searchTextReal( $rewritten ); if ( $rewrittenResult instanceof ResultSet && $rewrittenResult->numRows() > 0 ) { $rewrittenResult->setRewrittenQuery( $rewritten, $rewrittenSnippet ); if ( $rewrittenResult->numRows() < $GLOBALS['wgCirrusSearchInterwikiThreshold'] ) { // replace the result but still try the alt language $oldResult = $rewrittenResult; } else { return $rewrittenResult; } } } $altWiki = $this->hasSecondaryLanguage( $term ); if ( $altWiki ) { try { $config = new SearchConfig( $altWiki[0], $altWiki[1] ); } catch ( MWException $e ) { LoggerFactory::getInstance( 'CirrusSearch' )->info( "Failed to get config for {interwiki}:{dbwiki}", [ "interwiki" => $altWiki[0], "dbwiki" => $altWiki[1], "exception" => $e ] ); $config = null; } if ( $config ) { $matches = $this->searchTextReal( $term, $config ); if ( $matches instanceof ResultSet ) { $numRows = $matches->numRows(); $this->extraSearchMetrics['wgCirrusSearchAltLanguageNumResults'] = $numRows; // check whether we have second language functionality enabled. // This comes after the actual query is run so we can collect metrics about // users in the control buckets, and provide them the same latency as users // in the test bucket. if ( $GLOBALS['wgCirrusSearchEnableAltLanguage'] && $numRows > 0) { $oldResult->addInterwikiResults( $matches, SearchResultSet::INLINE_RESULTS, $altWiki[1] ); } } } } // Don't have any other options yet. return $oldResult; } /** * Do the hard part of the searching - actual Searcher invocation * @param string $term * @param SearchConfig $config * @return null|Status|ResultSet */ private function searchTextReal( $term, SearchConfig $config = null ) { global $wgCirrusSearchInterwikiSources; // Convert the unicode character 'ideographic whitespace' into standard // whitespace. Cirrussearch treats them both as normal whitespace, but // the preceding isn't appropriately trimmed. $term = trim( str_replace( "\xE3\x80\x80", " ", $term) ); // No searching for nothing! That takes forever! if ( $term === '' ) { return null; } if ( $config ) { $this->indexBaseName = $config->get( SearchConfig::INDEX_BASE_NAME ); } $searcher = new Searcher( $this->connection, $this->offset, $this->limit, $config, $this->namespaces, null, $this->indexBaseName ); // Ignore leading ~ because it is used to force displaying search results but not to effect them if ( substr( $term, 0, 1 ) === '~' ) { $term = substr( $term, 1 ); $searcher->addSuggestPrefix( '~' ); } // TODO remove this when we no longer have to support core versions without // Ie946150c6796139201221dfa6f7750c210e97166 if ( method_exists( $this, 'getSort' ) ) { $searcher->setSort( $this->getSort() ); } if ( isset( $this->features[SearchEngine::FT_QUERY_INDEP_PROFILE_TYPE] ) ) { $profile = $this->features[SearchEngine::FT_QUERY_INDEP_PROFILE_TYPE]; if ( $this->config->getElement( 'CirrusSearchRescoreProfiles', $profile ) !== null ) { $searcher->getSearchContext()->setRescoreProfile( $profile ); } } $dumpQuery = $this->request && $this->request->getVal( 'cirrusDumpQuery' ) !== null; $searcher->setReturnQuery( $dumpQuery ); $dumpResult = $this->request && $this->request->getVal( 'cirrusDumpResult' ) !== null; $searcher->setDumpResult( $dumpResult ); $returnExplain = $this->request && $this->request->getVal( 'cirrusExplain' ) !== null; $searcher->setReturnExplain( $returnExplain ); // Delegate to either searchText or moreLikeThisArticle and dump the result into $status if ( substr( $term, 0, strlen( self::MORE_LIKE_THIS_PREFIX ) ) === self::MORE_LIKE_THIS_PREFIX ) { $term = substr( $term, strlen( self::MORE_LIKE_THIS_PREFIX ) ); $status = $this->moreLikeThis( $term, $searcher, Searcher::MORE_LIKE_THESE_NONE ); } else if ( substr( $term, 0, strlen( self::MORE_LIKE_THIS_JUST_WIKIBASE_PREFIX ) ) === self::MORE_LIKE_THIS_JUST_WIKIBASE_PREFIX ) { $term = substr( $term, strlen( self::MORE_LIKE_THIS_JUST_WIKIBASE_PREFIX ) ); $status = $this->moreLikeThis( $term, $searcher, Searcher::MORE_LIKE_THESE_ONLY_WIKIBASE ); } else { # Namespace lookup should not be done for morelike special syntax (T111244) if ( $this->lastNamespacePrefix ) { $searcher->addSuggestPrefix( $this->lastNamespacePrefix ); } else { $searcher->updateNamespacesFromQuery( $term ); } $highlightingConfig = FullTextResultsType::HIGHLIGHT_ALL; if ( $this->request ) { if ( $this->request->getVal( 'cirrusSuppressSuggest' ) !== null ) { $this->showSuggestion = false; } if ( $this->request->getVal( 'cirrusSuppressTitleHighlight' ) !== null ) { $highlightingConfig ^= FullTextResultsType::HIGHLIGHT_TITLE; } if ( $this->request->getVal( 'cirrusSuppressAltTitle' ) !== null ) { $highlightingConfig ^= FullTextResultsType::HIGHLIGHT_ALT_TITLE; } if ( $this->request->getVal( 'cirrusSuppressSnippet' ) !== null ) { $highlightingConfig ^= FullTextResultsType::HIGHLIGHT_SNIPPET; } if ( $this->request->getVal( 'cirrusHighlightDefaultSimilarity' ) === 'no' ) { $highlightingConfig ^= FullTextResultsType::HIGHLIGHT_WITH_DEFAULT_SIMILARITY; } if ( $this->request->getVal( 'cirrusHighlightAltTitleWithPostings' ) === 'no' ) { $highlightingConfig ^= FullTextResultsType::HIGHLIGHT_ALT_TITLES_WITH_POSTINGS; } } if ( $this->namespaces && !in_array( NS_FILE, $this->namespaces ) ) { $highlightingConfig ^= FullTextResultsType::HIGHLIGHT_FILE_TEXT; } $searcher->setResultsType( new FullTextResultsType( $highlightingConfig, $config ? $config->getWikiCode() : '') ); $status = $searcher->searchText( $term, $this->showSuggestion ); } if ( $dumpQuery || $dumpResult ) { // When dumping the query we skip _everything_ but echoing the query. RequestContext::getMain()->getOutput()->disable(); if ( $this->request && $this->request->getVal( 'cirrusExplain' ) === 'pretty' ) { $printer = new CirrusSearch\ExplainPrinter(); echo $printer->format( $status->getValue() ); } else { $this->request->response()->header( 'Content-type: application/json; charset=UTF-8' ); if ( $status->getValue() === null ) { echo '{}'; } else { echo json_encode( $status->getValue() ); } } exit(); } $this->lastSearchMetrics = $searcher->getSearchMetrics(); // Add interwiki results, if we have a sane result // Note that we have no way of sending warning back to the user. In this case all warnings // are logged when they are added to the status object so we just ignore them here.... if ( $status->isOK() && $wgCirrusSearchInterwikiSources && $status->getValue() && method_exists( $status->getValue(), 'addInterwikiResults' ) ) { // @todo @fixme: This should absolutely be a multisearch. I knew this when I // wrote the code but Searcher needs some refactoring first. foreach ( $wgCirrusSearchInterwikiSources as $interwiki => $index ) { $iwSearch = new InterwikiSearcher( $this->connection, $this->namespaces, null, $index, $interwiki ); $interwikiResult = $iwSearch->getInterwikiResults( $term ); if ( $interwikiResult ) { $status->getValue()->addInterwikiResults( $interwikiResult, SearchResultSet::SECONDARY_RESULTS, $interwiki ); } } } // For historical reasons all callers of searchText interpret any Status return as an error // so we must unwrap all OK statuses. Note that $status can be "good" and still contain null // since that is interpreted as no results. return $status->isOK() ? $status->getValue() : $status; } /** * Look for suggestions using ES completion suggester. * @param string $search Search string * @param string[]|null $variants Search term variants * @param SearchConfig $config search configuration * @return SearchSuggestionSet Set of suggested names */ protected function getSuggestions( $search, $variants, SearchConfig $config ) { // Inspect features to check if the user selected a specific profile $profile = null; if ( isset( $this->features[SearchEngine::COMPLETION_PROFILE_TYPE] ) ) { $profile = $this->features[SearchEngine::COMPLETION_PROFILE_TYPE]; } // offset is omitted, searchSuggestion does not support // scrolling results $suggester = new CompletionSuggester( $this->connection, $this->limit, $this->offset, $config, $this->namespaces, null, $this->indexBaseName, $profile ); $response = $suggester->suggest( $search, $variants ); if ( $response->isOK() ) { // Errors will be logged, let's try the exact db match return $response->getValue(); } else { return SearchSuggestionSet::emptySuggestionSet(); } } /** * @param string $term * @param Searcher $searcher * @param int $options A bitset of Searcher::MORE_LIKE_THESE_* * @return Status<SearchResultSet> */ private function moreLikeThis( $term, $searcher, $options ) { // Expand titles chasing through redirects $titles = []; $found = []; foreach ( explode( '|', $term ) as $title ) { $title = Title::newFromText( trim( $title ) ); while ( true ) { if ( !$title ) { continue 2; } $titleText = $title->getFullText(); if ( in_array( $titleText, $found ) ) { continue 2; } $found[] = $titleText; if ( !$title->exists() ) { continue 2; } if ( $title->isRedirect() ) { $page = WikiPage::factory( $title ); if ( !$page->exists() ) { continue 2; } $title = $page->getRedirectTarget(); } else { break; } } $titles[] = $title; } if ( count( $titles ) ) { return $searcher->moreLikeTheseArticles( $titles, $options ); } return Status::newGood( new SearchResultSet( true ) /* empty */ ); } /** * Merge the prefix into the query (if any). * @param string $term search term * @return string possibly with a prefix appended */ public function transformSearchTerm( $term ) { if ( $this->prefix != '' ) { // Slap the standard prefix notation onto the query $term = $term . ' prefix:' . $this->prefix; } return $term; } /** * @param string $query * @return string */ public function replacePrefixes( $query ) { $parsed = parent::replacePrefixes( $query ); if ( $parsed !== $query ) { $this->lastNamespacePrefix = substr( $query, 0, strlen( $query ) - strlen( $parsed ) ); } else { $this->lastNamespacePrefix = ''; } return $parsed; } /** * Get the sort of sorts we allow * @return string[] */ public function getValidSorts() { return [ 'relevance', 'title_asc', 'title_desc' ]; } /** * Get the metrics for the last search we performed. Null if we haven't done any. * @return array */ public function getLastSearchMetrics() { /** @suppress PhanTypeMismatchReturn Phan doesn't handle array addition correctly */ return $this->lastSearchMetrics + $this->extraSearchMetrics; } protected function completionSuggesterEnabled( SearchConfig $config ) { $useCompletion = $config->getElement( 'CirrusSearchUseCompletionSuggester' ); if( $useCompletion !== 'yes' && $useCompletion !== 'beta' ) { return false; } // This way API can force-enable completion suggester if ( $this->isFeatureEnabled( self::COMPLETION_SUGGESTER_FEATURE ) ) { return true; } // Allow falling back to prefix search with query param if ( $this->request && $this->request->getVal( 'cirrusUseCompletionSuggester' ) === 'no' ) { return false; } // Allow experimentation with query parameters if ( $this->request && $this->request->getVal( 'cirrusUseCompletionSuggester' ) === 'yes' ) { return true; } if ( $useCompletion === 'beta' ) { return class_exists( '\BetaFeatures' ) && \BetaFeatures::isFeatureEnabled( $GLOBALS['wgUser'], 'cirrussearch-completionsuggester' ); } return true; } /** * Perform a completion search. * Does not resolve namespaces and does not check variants. * We use parent search for: * - Special: namespace * We use old prefix search for: * - Suggester not enabled * - * @param string $search * @return SearchSuggestionSet */ protected function completionSearchBackend( $search ) { if ( in_array( NS_SPECIAL, $this->namespaces ) ) { // delegate special search to parent return parent::completionSearchBackend( $search ); } if ( !$this->completionSuggesterEnabled( $this->config ) ) { // Completion suggester is not enabled, fallback to // default implementation return $this->prefixSearch( $search ); } if ( count( $this->namespaces ) != 1 || reset( $this->namespaces ) != NS_MAIN ) { // for now, suggester only works for main namespace return $this->prefixSearch( $search ); } if ( isset( $this->features[SearchEngine::COMPLETION_PROFILE_TYPE] ) ) { // Fallback to prefixsearch if the classic profile was selected. if ( $this->features[SearchEngine::COMPLETION_PROFILE_TYPE] == self::COMPLETION_PREFIX_FALLBACK_PROFILE ) { return $this->prefixSearch( $search ); } } // Not really useful, mostly for testing purpose $variants = $this->request->getArray( 'cirrusCompletionSuggesterVariant' ); if ( empty( $variants ) ) { global $wgContLang; $variants = $wgContLang->autoConvertToAllVariants( $search ); } else if ( count( $variants ) > 3 ) { // We should not allow too many variants $variants = array_slice( $variants, 0, 3 ); } return $this->getSuggestions( $search, $variants, $this->config ); } /** * Override variants function because we always do variants * in the backend. * @see SearchEngine::completionSearchWithVariants() * @param string $search * @return SearchSuggestionSet */ public function completionSearchWithVariants( $search ) { return $this->completionSearch( $search ); } /** * Older prefix search. * @param string $search search text * @return SearchSuggestionSet */ protected function prefixSearch( $search ) { $searcher = new Searcher( $this->connection, $this->offset, $this->limit, null, $this->namespaces ); if ( $search ) { $searcher->setResultsType( new FancyTitleResultsType( 'prefix' ) ); } else { // Empty searches always find the title. $searcher->setResultsType( new TitleResultsType() ); } try { $status = $searcher->prefixSearch( $search ); } catch ( UsageException $e ) { if ( defined( 'MW_API' ) ) { throw $e; } return SearchSuggestionSet::emptySuggestionSet(); } // There is no way to send errors or warnings back to the caller here so we have to make do with // only sending results back if there are results and relying on the logging done at the status // construction site to log errors. if ( $status->isOK() ) { if ( !$search ) { // No need to unpack the simple title matches from non-fancy TitleResultsType return SearchSuggestionSet::fromTitles( $status->getValue() ); } $results = array_filter( array_map( function( $match ) { if ( isset( $match[ 'titleMatch' ] ) ) { return $match[ 'titleMatch' ]; } else { if ( isset( $match[ 'redirectMatches' ][ 0 ] ) ) { // TODO maybe dig around in the redirect matches and find the best one? return $match[ 'redirectMatches' ][0]; } } return false; }, $status->getValue() ) ); return SearchSuggestionSet::fromTitles( $results ); } return SearchSuggestionSet::emptySuggestionSet(); } /** * {@inheritDoc} */ public function getProfiles( $profileType ) { switch( $profileType ) { case SearchEngine::COMPLETION_PROFILE_TYPE: if ( $this->config->get( 'CirrusSearchUseCompletionSuggester' ) == 'no' ) { // No profile selection if completion suggester is disabled. return []; } $profiles = []; foreach( array_keys( $this->config->get( 'CirrusSearchCompletionProfiles' ) ) as $name ) { $profiles[] = [ 'name' => $name, 'desc-message' => 'cirrussearch-completion-profile-' . $name, 'default' => $this->config->get( 'CirrusSearchCompletionSettings' ) == $name, ]; } // Add fallback to prefixsearch $profiles[] = [ 'name' => self::COMPLETION_PREFIX_FALLBACK_PROFILE, 'desc-message' => 'cirrussearch-completion-profile-' . self::COMPLETION_PREFIX_FALLBACK_PROFILE, 'default' => false, ]; return $profiles; case SearchEngine::FT_QUERY_INDEP_PROFILE_TYPE: $profiles = []; // The Hook should run profiles/RescoreProfiles.php before // any consumer call to getProfiles, so that the default // will be properly set when the curstom request param // cirrusRescoreProfile=profile is set. $cirrusDefault = $this->config->get( 'CirrusSearchRescoreProfile' ); $defaultFound = false; foreach( $this->config->get( 'CirrusSearchRescoreProfiles' ) as $name => $profile ) { $default = $cirrusDefault === $name; $defaultFound |= $default; $profiles[] = [ 'name' => $name, // @todo: decide what to with profiles we declare // in wmf-config with no i18n messages. // Do we want to expose them anyway, or simply // hide them but still allow Api to pass them to us. // It may require a change in core since ApiBase is // strict and won't allow unknown values to be set // here. 'desc-message' => isset ( $profile['i18n_msg'] ) ? $profile['i18n_msg'] : null, 'default' => $default, ]; } return $profiles; } return null; } /** * Create a search field definition * @param string $name * @param int $type * @return SearchIndexField */ public function makeSearchFieldMapping( $name, $type ) { $overrides = $this->config->get( 'CirrusSearchFieldTypeOverrides' ); $mappings = $this->config->get( 'CirrusSearchFieldTypes' ); if ( !isset( $mappings[$type] ) ) { return new NullIndexField(); } $klass = $mappings[$type]; // Check if a specific class is provided for this field if ( isset( $overrides[$name] ) ) { if ( $klass !== $overrides[$name] && !is_subclass_of( $overrides[$name], $klass ) ) { throw new \Exception( "Specialized class " . $overrides[$name] . " for field $name is not compatible with type class $klass" ); } $klass = $overrides[$name]; } return new $klass( $name, $type, $this->config ); } }