Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 32 |
|
0.00% |
0 / 7 |
CRAP | |
0.00% |
0 / 1 |
TemplateCollectionFeature | |
0.00% |
0 / 32 |
|
0.00% |
0 / 7 |
156 | |
0.00% |
0 / 1 |
getKeywords | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
__construct | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
addCollection | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
parseValue | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
20 | |||
doApply | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
getFilterQuery | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
doGetFilterQuery | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace GrowthExperiments; |
4 | |
5 | use CirrusSearch\Parser\AST\KeywordFeatureNode; |
6 | use CirrusSearch\Query\Builder\QueryBuildingContext; |
7 | use CirrusSearch\Query\FilterQueryFeature; |
8 | use CirrusSearch\Query\QueryHelper; |
9 | use CirrusSearch\Query\SimpleKeywordFeature; |
10 | use CirrusSearch\Search\Filters; |
11 | use CirrusSearch\Search\SearchContext; |
12 | use CirrusSearch\WarningCollector; |
13 | use Elastica\Query\AbstractQuery; |
14 | use GrowthExperiments\Config\Validation\GrowthConfigValidation; |
15 | use MediaWiki\Linker\LinkTarget; |
16 | use MediaWiki\Title\TitleFactory; |
17 | use MediaWiki\Title\TitleValue; |
18 | use Wikimedia\Assert\Assert; |
19 | |
20 | /** |
21 | * Like HasTemplateFeature, but operates on a collection of templates from a pre-defined list |
22 | * instead of directly via user input. The search is case-sensitive, so the templates in the |
23 | * pre-defined list should be also. HasTemplateFeature isn't extended directly to avoid strong |
24 | * coupling. |
25 | */ |
26 | class TemplateCollectionFeature extends SimpleKeywordFeature implements FilterQueryFeature { |
27 | |
28 | /** |
29 | * ElasticSearch doesn't allow more than 1024 clauses in a boolean query. Limit at 800 |
30 | * to leave some space to combine hastemplatecollection with other search terms. |
31 | */ |
32 | public const MAX_TEMPLATES_IN_COLLECTION = GrowthConfigValidation::MAX_TEMPLATES_IN_COLLECTION; |
33 | |
34 | private array $templates; |
35 | private TitleFactory $titleFactory; |
36 | |
37 | /** @inheritDoc */ |
38 | protected function getKeywords() { |
39 | return [ 'hastemplatecollection' ]; |
40 | } |
41 | |
42 | /** |
43 | * @param string $collectionName |
44 | * @param string[]|LinkTarget[] $templates |
45 | * @param TitleFactory $titleFactory |
46 | */ |
47 | public function __construct( string $collectionName, array $templates, TitleFactory $titleFactory ) { |
48 | $this->titleFactory = $titleFactory; |
49 | $this->addCollection( $collectionName, $templates ); |
50 | } |
51 | |
52 | /** |
53 | * @param string $collectionName |
54 | * @param string[]|LinkTarget[] $templates |
55 | */ |
56 | public function addCollection( string $collectionName, array $templates ): void { |
57 | Assert::parameter( |
58 | count( $templates ) <= self::MAX_TEMPLATES_IN_COLLECTION, |
59 | '$templates', |
60 | 'Maximum ' . self::MAX_TEMPLATES_IN_COLLECTION . ' templates allowed in collection.' |
61 | ); |
62 | $this->templates[$collectionName] = $templates; |
63 | } |
64 | |
65 | /** @inheritDoc */ |
66 | public function parseValue( |
67 | $key, $value, $quotedValue, $valueDelimiter, $suffix, WarningCollector $warningCollector |
68 | ) { |
69 | // If an undefined collection name is used in the user-provided input, then just return no results. |
70 | if ( !isset( $this->templates[$value] ) ) { |
71 | $warningCollector->addWarning( 'growthexperiments-templatecollectionfeature-invalid-collection', |
72 | $value ); |
73 | return [ 'templates' => [] ]; |
74 | } |
75 | $templates = []; |
76 | foreach ( $this->templates[$value] as $template ) { |
77 | if ( $template instanceof TitleValue ) { |
78 | $title = $this->titleFactory->newFromLinkTarget( $template ); |
79 | } else { |
80 | $title = $this->titleFactory->newFromText( $template ); |
81 | } |
82 | $templates[] = $title->getPrefixedText(); |
83 | } |
84 | return [ 'templates' => $templates ]; |
85 | } |
86 | |
87 | /** @inheritDoc */ |
88 | protected function doApply( SearchContext $context, $key, $value, $quotedValue, $negated ) { |
89 | $filter = $this->doGetFilterQuery( |
90 | $this->parseValue( $key, $value, $quotedValue, '', '', $context ) ); |
91 | if ( $filter === null && !$negated ) { |
92 | // If there are no templates in a collection, it shouldn't match anything. If the |
93 | // keyword is negated, it should be a no-op, so returning null works. |
94 | $context->setResultsPossible( false ); |
95 | } |
96 | return [ $filter, false ]; |
97 | } |
98 | |
99 | /** @inheritDoc */ |
100 | public function getFilterQuery( KeywordFeatureNode $node, QueryBuildingContext $context ) { |
101 | // TODO handle the null case once CirrusSearch starts using this method. |
102 | return $this->doGetFilterQuery( $node->getParsedValue() ); |
103 | } |
104 | |
105 | /** |
106 | * @param string[][] $parsedValue |
107 | * @return AbstractQuery|null |
108 | */ |
109 | protected function doGetFilterQuery( array $parsedValue ) { |
110 | return Filters::booleanOr( array_map( |
111 | static function ( $v ) { |
112 | return QueryHelper::matchPage( 'template.keyword', $v ); |
113 | }, |
114 | $parsedValue['templates'] |
115 | ), false ); |
116 | } |
117 | |
118 | } |