Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
76.24% covered (warning)
76.24%
77 / 101
44.44% covered (danger)
44.44%
4 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
SchemaTreeSuggester
76.24% covered (warning)
76.24%
77 / 101
44.44% covered (danger)
44.44%
4 / 9
35.07
0.00% covered (danger)
0.00%
0 / 1
 setDeprecatedPropertyIds
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setClassifyingPropertyIds
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setSchemaTreeSuggesterUrl
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setEventLogger
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getSuggestions
86.84% covered (warning)
86.84%
33 / 38
0.00% covered (danger)
0.00%
0 / 1
7.11
 suggestByPropertyIds
92.31% covered (success)
92.31%
12 / 13
0.00% covered (danger)
0.00%
0 / 1
1.00
 suggestByItem
54.29% covered (warning)
54.29%
19 / 35
0.00% covered (danger)
0.00%
0 / 1
11.68
 buildResult
90.00% covered (success)
90.00%
9 / 10
0.00% covered (danger)
0.00%
0 / 1
6.04
1<?php
2
3namespace PropertySuggester\Suggesters;
4
5use Exception;
6use InvalidArgumentException;
7use LogicException;
8use MediaWiki\Http\HttpRequestFactory;
9use PropertySuggester\EventLogger;
10use Wikibase\DataModel\Entity\EntityIdValue;
11use Wikibase\DataModel\Entity\Item;
12use Wikibase\DataModel\Entity\ItemId;
13use Wikibase\DataModel\Entity\NumericPropertyId;
14use Wikibase\DataModel\Snak\PropertyValueSnak;
15
16/**
17 * a Suggester implementation that creates suggestions using
18 * the SchemaTree suggester. Requires the PropertySuggesterSchemaTreeUrl
19 * to be defined in the configuration.
20 *
21 * @license GPL-2.0-or-later
22 */
23class SchemaTreeSuggester implements SuggesterEngine {
24
25    /**
26     * @var int[]
27     */
28    private $deprecatedPropertyIds = [];
29
30    /**
31     * @var array Numeric property ids as keys, values are meaningless.
32     */
33    private $classifyingPropertyIds = [];
34
35    /**
36     * @var string
37     */
38    private $schemaTreeSuggesterUrl;
39
40    /**
41     * @var EventLogger|null
42     */
43    private $eventLogger;
44
45    /**
46     * @param int[] $deprecatedPropertyIds
47     */
48    public function setDeprecatedPropertyIds( array $deprecatedPropertyIds ) {
49        $this->deprecatedPropertyIds = $deprecatedPropertyIds;
50    }
51
52    /**
53     * @param int[] $classifyingPropertyIds
54     */
55    public function setClassifyingPropertyIds( array $classifyingPropertyIds ) {
56        $this->classifyingPropertyIds = array_flip( $classifyingPropertyIds );
57    }
58
59    /**
60     * @param string $schemaTreeSuggesterUrl
61     */
62    public function setSchemaTreeSuggesterUrl( string $schemaTreeSuggesterUrl ) {
63        $this->schemaTreeSuggesterUrl = $schemaTreeSuggesterUrl;
64    }
65
66    /**
67     * @var HttpRequestFactory
68     */
69    private $httpFactory;
70
71    /**
72     * @param EventLogger $eventLogger
73     */
74    public function setEventLogger( EventLogger $eventLogger ) {
75        $this->eventLogger = $eventLogger;
76    }
77
78    public function __construct( HttpRequestFactory $httpFactory ) {
79        $this->httpFactory = $httpFactory;
80    }
81
82    /**
83     * @param int[] $propertyIds
84     * @param int[] $typesIds
85     * @param int $limit
86     * @param float $minProbability
87     * @param string $include
88     * @return Suggestion[]|null
89     * @throws InvalidArgumentException
90     * @throws Exception
91     */
92    private function getSuggestions(
93        array $propertyIds,
94        array $typesIds,
95        int $limit,
96        float $minProbability,
97        string $include
98    ) {
99        $this->eventLogger->setPropertySuggesterName( 'SchemaTreeSuggester' );
100        $startTime = microtime( true );
101
102        if ( !in_array( $include, [ self::SUGGEST_ALL, self::SUGGEST_NEW ] ) ) {
103            throw new InvalidArgumentException( '$include must be one of the SUGGEST_* constants!' );
104        }
105        $excludedIds = [];
106        if ( $include === self::SUGGEST_NEW ) {
107            $excludedIds = array_merge( $propertyIds, $this->deprecatedPropertyIds );
108        }
109        $excludedIds = array_map( static function ( int $id ) {
110            return 'P' . $id;
111        }, $excludedIds );
112
113        $properties = [];
114        foreach ( $propertyIds as $id ) {
115            $properties[] = 'P' . $id;
116        }
117
118        $types = [];
119        foreach ( $typesIds as $id ) {
120            $types[] = 'Q' . $id;
121        }
122
123        $response = $this->httpFactory->post(
124            $this->schemaTreeSuggesterUrl,
125            [
126                'postData' => json_encode( [
127                    'Properties' => $properties,
128                    'Types' => $types
129                ] ),
130                'timeout' => 1
131            ],
132            __METHOD__
133        );
134
135        // if request fails fall back to original property suggester
136        if ( !$response ) {
137            $this->eventLogger->setRequestDuration( -1 );
138            $this->eventLogger->logEvent();
139            return null;
140        }
141
142        $result = json_decode( $response, true );
143
144        $result = $result['recommendations'] ?? null;
145        if ( !is_array( $result ) ) {
146            return null;
147        }
148
149        $results = $this->buildResult( $result, $minProbability, $excludedIds, $limit );
150        $this->eventLogger->setRequestDuration( (int)( ( microtime( true ) - $startTime ) * 1000 ) );
151        return $results;
152    }
153
154    /**
155     * @see SuggesterEngine::suggestByPropertyIds
156     *
157     * @param NumericPropertyId[] $propertyIds
158     * @param ItemId[] $typesIds
159     * @param int $limit
160     * @param float $minProbability
161     * @param string $context
162     * @param string $include One of the self::SUGGEST_* constants
163     * @return Suggestion[]|null
164     */
165    public function suggestByPropertyIds(
166        array $propertyIds,
167        array $typesIds,
168        $limit,
169        $minProbability,
170        $context,
171        $include
172    ) {
173        $numericIds = array_map( static function ( NumericPropertyId $propertyId ) {
174            return $propertyId->getNumericId();
175        }, $propertyIds );
176
177        $numericTypeIds = array_map( static function ( ItemId $typeId ) {
178            return $typeId->getNumericId();
179        }, $typesIds );
180
181        return $this->getSuggestions(
182            $numericIds,
183            $numericTypeIds,
184            $limit,
185            $minProbability,
186            $include
187        );
188    }
189
190    /**
191     * @see SuggesterEngine::suggestByEntity
192     *
193     * @param Item $item
194     * @param int $limit
195     * @param float $minProbability
196     * @param string $context
197     * @param string $include One of the self::SUGGEST_* constants
198     * @return Suggestion[]|null
199     * @throws LogicException|Exception
200     */
201    public function suggestByItem( Item $item, $limit, $minProbability, $context, $include ) {
202        $ids = [];
203        $types = [];
204
205        foreach ( $item->getStatements()->toArray() as $statement ) {
206            $mainSnak = $statement->getMainSnak();
207
208            $id = $mainSnak->getPropertyId();
209            if ( !( $id instanceof NumericPropertyId ) ) {
210                throw new LogicException( 'PropertySuggester is incompatible with non-numeric Property IDs' );
211            }
212
213            $numericPropertyId = $id->getNumericId();
214            $ids[] = $numericPropertyId;
215
216            if ( isset( $this->classifyingPropertyIds[$numericPropertyId] )
217                && ( $mainSnak instanceof PropertyValueSnak ) ) {
218                $dataValue = $mainSnak->getDataValue();
219
220                if ( !( $dataValue instanceof EntityIdValue ) ) {
221                    throw new LogicException(
222                        "Property $numericPropertyId in wgPropertySuggesterClassifyingPropertyIds"
223                        . ' does not have value type wikibase-entityid'
224                    );
225                }
226
227                $entityId = $dataValue->getEntityId();
228
229                if ( !( $entityId instanceof ItemId ) ) {
230                    throw new LogicException(
231                        "PropertyValueSnak for $numericPropertyId, configured in " .
232                        ' wgPropertySuggesterClassifyingPropertyIds, has an unexpected value ' .
233                        'and data type (not wikibase-item).'
234                    );
235                }
236                $numericEntityId = $entityId->getNumericId();
237                $types[] = $numericEntityId;
238            }
239        }
240
241        $this->eventLogger->setExistingProperties( array_map( 'strval', $ids ) );
242        $this->eventLogger->setExistingTypes( array_map( 'strval', $types ) );
243
244        return $this->getSuggestions(
245            $ids,
246            $types,
247            $limit,
248            $minProbability,
249            $include
250        );
251    }
252
253    /**
254     * Converts the JSON object results to Suggestion objects
255     * @param array $response
256     * @param float $minProbability
257     * @param array $excludedIds
258     * @param int $limit
259     * @return Suggestion[]
260     */
261    private function buildResult( array $response, float $minProbability, array $excludedIds, int $limit ): array {
262        $resultArray = [];
263        foreach ( $response as $pos => $res ) {
264            if ( $pos > $limit ) {
265                break;
266            }
267            if ( $res['probability'] > $minProbability && strpos( $res['property'], 'P' ) === 0 ) {
268                if ( !in_array( $res['property'], $excludedIds ) ) {
269                    $pid = new NumericPropertyId( $res['property'] );
270                    $suggestion = new Suggestion( $pid, $res["probability"] );
271                    $resultArray[] = $suggestion;
272                }
273            }
274        }
275        return $resultArray;
276    }
277}