Passed
Push — master ( 3a7c7a...fe0bb9 )
by Sam
03:04
created

Item::getInstanceOf()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 3
ccs 0
cts 3
cp 0
crap 2
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Samwilson\SimpleWikidata;
4
5
use DateInterval;
6
use Exception;
7
use Mediawiki\Api\MediawikiApi;
8
use Mediawiki\Api\SimpleRequest;
9
use Nayjest\StrCaseConverter\Str;
10
use Psr\Cache\CacheItemPoolInterface;
11
use Samwilson\SimpleWikidata\Properties\Time;
12
use Symfony\Component\DomCrawler\Crawler;
13
14
class Item {
15
16
	const PROP_INSTANCE_OF = 'P31';
17
	const PROP_TITLE = 'P1476';
18
	const PROP_IMAGE = 'P18';
19
	const PROP_AUTHOR = 'P50';
20
21
	/** @var string */
22
	protected $id;
23
24
	/** @var MediawikiApi */
25
	protected $wdApi;
26
27
	/** @var string */
28
	protected $lang;
29
30
	/** @var CacheItemPoolInterface */
31
	protected $cache;
32
33
	/** @var string The base URL of Wikidata, with trailing slash. */
34
	protected $wikidataUrlBase = 'https://www.wikidata.org/wiki/';
35
36 1
		private function __construct( $id, $lang, CacheItemPoolInterface $cache ) {
37 1
		if ( !is_string( $id ) || preg_match( '/[QP][0-9]*/i', $id ) !== 1 ) {
38
			throw new Exception( "Not a valid ID: " . var_export( $id, true ) );
39
		}
40 1
		$this->id = $id;
41 1
		$this->wdApi = new MediawikiApi( 'https://www.wikidata.org/w/api.php' );
42 1
		$this->entities = [];
0 ignored issues
show
Bug Best Practice introduced by
The property entities does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
43 1
		$this->lang = $lang;
44 1
		$this->cache = $cache;
45 1
	 }
46
47
	/**
48
	 * Create a new Item object with class based on the item's 'instance of' statement.
49
	 *
50
	 * @param string $id
51
	 * @param string $lang
52
	 *
53
	 * @return Item
54
	 */
55 1
	public static function factory( $id, $lang, CacheItemPoolInterface $cache ) {
56 1
		$item = new Item( $id, $lang, $cache );
57 1
		foreach ( $item->getPropertyOfTypeItem( self::PROP_INSTANCE_OF ) as $instanceOf ) {
58
			// Try to find a class mating the 'instance of' name.
59 1
			$possibleClassName = __NAMESPACE__ . '\\Items\\' . Str::toCamelCase( $instanceOf->getItem()->getLabel() );
60 1
			if ( class_exists( $possibleClassName ) ) {
61
				// This won't re-request the metadata, because that's cached.
62
				$specificItem = new $possibleClassName( $id, $lang, $cache );
63 1
				return $specificItem;
64
			}
65
		}
66
67
		// If we're here, just leave it as a basic Item.
68 1
		$item->setCache( $cache );
69 1
		return $item;
70
	}
71
72 1
	public function setCache( CacheItemPoolInterface $cache_item_pool ) {
73 1
		$this->cache = $cache_item_pool;
74 1
	}
75
76
	/**
77
	 * Get the ID (Q-number) of this item.
78
	 * @return string|bool The ID or false if it couldn't be determined.
79
	 */
80 1
	public function getId() {
81 1
		$entity = $this->getEntity( $this->id );
82 1
		return isset( $entity['id'] ) ? $entity['id'] : false;
83
	}
84
85
	/**
86
	 * Get this item's label.
87
	 * @return string
88
	 */
89 1
	public function getLabel() {
90 1
		$entity = $this->getEntity( $this->id );
91 1
		if ( ! empty( $entity['labels'][ $this->lang ]['value'] ) ) {
92
			// Use the label if there is one.
93 1
			return $entity['labels'][ $this->lang ]['value'];
94
		}
95
		// Or just use the ID.
96 1
		return $entity['id'];
97
	}
98
99 1
	public function getWikidataUrl() {
100 1
		return $this->wikidataUrlBase.$this->id;
101
	}
102
103
	public function getStandardProperties( $type = 'work' ) {
104
		if ( $type !== 'work' ) {
105
			$type = 'edition';
106
		}
107
		$cacheKey = $type . '_item_property_IDs';
108
		if ( $this->cache->hasItem( $cacheKey ) ) {
109
			$propIds = $this->cache->getItem( $cacheKey )->get();
110
		} else {
111
			$domCrawler = new Crawler();
112
			$domCrawler->addHtmlContent( file_get_contents( 'https://www.wikidata.org/wiki/Wikidata:WikiProject_Books' ) );
113
			$propCells = $domCrawler->filterXPath( "//h3/span[@id='" . ucfirst( $type ) . "_item_properties']/../following-sibling::table[1]//td[2]/a" );
114
			$propIds = [];
115
			$propCells->each( function ( Crawler $node, $i ) use ( &$propIds ) {
116
				$propId = $node->text();
117
				$propIds[] = $propId;
118
			} );
119
			$cacheItem = $this->cache->getItem( $cacheKey )
120
				->expiresAfter( new DateInterval( 'PT1H' ) )
121
				->set( $propIds );
122
			$this->cache->save( $cacheItem );
123
		}
124
		$workProperties = [];
125
		foreach ( $propIds as $propId ) {
126
			$workProperties[] = self::factory( $propId, $this->lang, $this->cache );
127
		}
128
129
		return $workProperties;
130
	}
131
132
	/**
133
	 * @param string $propertyId
134
	 * @return bool|Time[]
135
	 */
136 View Code Duplication
	protected function getPropertyOfTypeTime( $propertyId ) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
137
		$times = [];
138
		$entity = $this->getEntity();
139
		if ( !isset( $entity['claims'][$propertyId] ) ) {
140
			// No statements for this property.
141
			return $times;
142
		}
143
		// print_r($entity['claims'][$propertyId]);exit();
0 ignored issues
show
Unused Code Comprehensibility introduced by
88% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
144
		foreach ( $entity['claims'][$propertyId] as $claim ) {
145
			// print_r($claim);
0 ignored issues
show
Unused Code Comprehensibility introduced by
67% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
146
			$times[] = new Time( $claim, $this->lang, $this->cache );
147
//
0 ignored issues
show
Unused Code Comprehensibility introduced by
55% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
148
// $timeValue = $claim['datavalue']['value']['time'];
149
// // Ugly workaround for imprecise dates. :-(
150
// if (preg_match('/([0-9]{1,4})-00-00/', $timeValue, $matches) === 1) {
151
// $timeValue = $matches[1];
152
// return $timeValue;
153
// }
154
// $time = strtotime($timeValue);
155
// return date($dateFormat, $time);
156
			// }
157
		}
158
		return $times;
159
	}
160
161
	/**
162
	 * Get the Item that is referred to by the specified item's property.
163
	 *
164
	 * @param string $itemId
165
	 * @param string $propertyId
166
	 *
167
	 * @return \Samwilson\SimpleWikidata\Properties\Item[]
168
	 */
169 1 View Code Duplication
	protected function getPropertyOfTypeItem( $propertyId ) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
170 1
		$entity = $this->getEntity( $this->id );
171 1
		if ( !isset( $entity['claims'][$propertyId] ) ) {
172 1
			return [];
173
		}
174 1
		$items = [];
175 1
		foreach ( $entity['claims'][$propertyId] as $claim ) {
176 1
			$items[] = new Properties\Item( $claim, $this->lang, $this->cache );
177
		}
178
179 1
		return $items;
180
	}
181
182
	public function setPropertyOfTypeItem( $property, $itemId ) {
183
		$itemIdNumeric = substr( $itemId, 1 );
184
185
		// First see if this property already exists, and that it is different from what's being set.
186
		$entity = $this->getEntity( $this->id );
187
		if ( !empty( $entity['claims'][$property] ) ) {
188
			// Get the first claim, and update it if necessary.
189
			$claim = array_shift( $entity['claims'][$property] );
190
			if ( $claim['mainsnak']['datavalue']['value']['id'] == $itemId ) {
191
				// Already is the required value, no need to change.
192
				return;
193
			}
194
			$claim['mainsnak']['datavalue']['value']['id'] = $itemId;
195
			$claim['mainsnak']['datavalue']['value']['numeric-id'] = $itemIdNumeric;
196
			$apiParams = [
197
				'action' => 'wbsetclaim',
198
				'claim' => wp_json_encode( $claim ),
0 ignored issues
show
Bug introduced by
The function wp_json_encode was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

198
				'claim' => /** @scrutinizer ignore-call */ wp_json_encode( $claim ),
Loading history...
199
			];
200
		}
201
202
		// If no claim was found (and modified) above, create a new claim.
203 View Code Duplication
		if ( !isset( $apiParams ) ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
204
			$apiParams = [
205
				'action' => 'wbcreateclaim',
206
				'entity' => $this->getId(),
207
				'property' => $property,
208
				'snaktype' => 'value',
209
				'value' => wp_json_encode( [ 'entity-type' => 'item', 'numeric-id' => $itemIdNumeric ] ),
210
			];
211
		}
212
213
		// Save the property.
214
		$wdWpOauth = new WdWpOauth();
0 ignored issues
show
Bug introduced by
The type Samwilson\SimpleWikidata\WdWpOauth was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
215
		$wdWpOauth->makeCall( $apiParams, true );
216
217
		// Clear the cache.
218
		$this->cache->deleteItem( $this->getEntityCacheKey( $this->id ) );
219
	}
220
221
	public function getPropertyOfTypeUrl( $entityId, $propertyId ) {
222
		$entity = $this->getEntity( $entityId );
223
		if ( !isset( $entity['claims'][$propertyId] ) ) {
224
			return false;
225
		}
226
		$urls = [];
227
		foreach ( $entity['claims'][$propertyId] as $claim ) {
228
			$urls[] = $claim['mainsnak']['datavalue']['value'];
229
		}
230
231
		return $urls;
232
	}
233
234
	public function getPropertyOfTypeExternalIdentifier( $entityId, $propertyId ) {
235
		$entity = $this->getEntity( $entityId );
236
		if ( !isset( $entity['claims'][$propertyId] ) ) {
237
			return false;
238
		}
239
		$idents = [];
240
		foreach ( $entity['claims'][$propertyId] as $claim ) {
241
			$qualifiers = [];
242
			if ( !isset( $claim['qualifiers'] ) ) {
243
				continue;
244
			}
245
			foreach ( $claim['qualifiers'] as $qualsInfo ) {
246
				foreach ( $qualsInfo as $qualInfo ) {
247
					$qualProp = self::factory( $qualInfo['property'], $this->lang, $this->cache );
248
					$propLabel = $qualProp->getLabel();
249
					if ( !isset( $qualifiers[$propLabel] ) ) {
250
						$qualifiers[$propLabel] = [];
251
					}
252
					$qualifiers[$propLabel][] = $qualInfo['datavalue']['value'];
253
				}
254
			}
255
			$idents[] = [
256
				'qualifiers' => $qualifiers,
257
				'value' => $claim['mainsnak']['datavalue']['value'],
258
			];
259
		}
260
261
		return $idents;
262
	}
263
264
	/**
265
	 * Get a single-valued text property.
266
	 * @param string $property One of the PROP_* constants.
267
	 * @return string|bool The value, or false if it can't be found.
268
	 */
269
	public function getPropertyOfTypeText( $property ) {
270
		$entity = $this->getEntity( $this->id );
271
		if ( isset( $entity['claims'][$property] ) ) {
272
			// Use the first title.
273
			foreach ( $entity['claims'][$property] as $t ) {
274
				if ( !isset( $t['mainsnak']['datavalue']['value']['language'] ) ) {
275
					var_dump( $t['mainsnak']['datavalue']['value'] );
0 ignored issues
show
Security Debugging Code introduced by
var_dump($t['mainsnak']['datavalue']['value']) looks like debug code. Are you sure you do not want to remove it?
Loading history...
276
					exit();
0 ignored issues
show
Coding Style Compatibility introduced by
The method getPropertyOfTypeText() contains an exit expression.

An exit expression should only be used in rare cases. For example, if you write a short command line script.

In most cases however, using an exit expression makes the code untestable and often causes incompatibilities with other libraries. Thus, unless you are absolutely sure it is required here, we recommend to refactor your code to avoid its usage.

Loading history...
277
				}
278
				if ( $t['mainsnak']['datavalue']['value']['language'] == $this->lang
279
					&& !empty( $t['mainsnak']['datavalue']['value']['text'] )
280
				) {
281
					return $t['mainsnak']['datavalue']['value']['text'];
282
				}
283
			}
284
		}
285
		return false;
286
	}
287
288
	/**
289
	 * Literal data field for a quantity that relates to some kind of well-defined unit. The actual unit goes in the data values that is entered.
290
	 *   - amount – implicit part of the string (mapping of unit prefix is unclear)
291
	 *   - unit – implicit part of the string that defaults to "1" (mapping to standardizing body is unclear)
292
	 *   - upperbound - quantity's upper bound
293
	 *   - lowerbound - quantity's lower bound
294
	 * @param $property
295
	 * @return mixed[]|bool If it's not false it's an array with 'amount', 'unit', etc.
296
	 */
297
	public function getPropertyOfTypeQuantity( $property ) {
298
		$quantities = [];
299
		$entity = $this->getEntity( $this->id );
300
		if ( !isset( $entity['claims'][$property] ) ) {
301
			return false;
302
		}
303
		foreach ( $entity['claims'][$property] as $t ) {
304
			$quantity = $t['mainsnak']['datavalue']['value'];
305
			$unitId = substr( $quantity['unit'], strlen( $this->wikidataUrlBase ) + 1 );
306
			$quantity['unit'] = self::factory( $unitId, $this->lang, $this->cache );
307
			$quantities[] = $quantity;
308
		}
309
		return $quantities;
310
	}
311
312
	/**
313
	 * Set a single-valued text property.
314
	 * @param string $property One of the PROP_* constants.
315
	 * @param string $value The value.
316
	 */
317
	public function setPropertyOfTypeText( $property, $value ) {
318
		// First see if this property already exists, and that it is different from what's being set.
319
		$entity = $this->getEntity( $this->id );
320
		if ( !empty( $entity['claims'][$property] ) ) {
321
			// Find this language's claim (if there is one).
322
			foreach ( $entity['claims'][$property] as $claim ) {
323
				if ( $claim['mainsnak']['datavalue']['value']['language'] == $this->lang ) {
324
					// Modify this claim's text value.
325
					$titleClaim = $claim;
326
					$titleClaim['mainsnak']['datavalue']['value']['text'] = $value;
327
					$setTitleParams = [
328
						'action' => 'wbsetclaim',
329
						'claim' => \GuzzleHttp\json_encode( $titleClaim ),
330
					];
331
					continue;
332
				}
333
			}
334
		}
335
336
		// If no claim was found (and modified) above, create a new claim.
337 View Code Duplication
		if ( !isset( $setTitleParams ) ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
338
			$setTitleParams = [
339
				'action' => 'wbcreateclaim',
340
				'entity' => $this->getId(),
341
				'property' => $property,
342
				'snaktype' => 'value',
343
				'value' => \GuzzleHttp\json_encode( [ 'text' => $value, 'language' => $this->lang ] ),
344
			];
345
		}
346
347
		// Save the property.
348
		$wdWpOauth = new WdWpOauth();
349
		$wdWpOauth->makeCall( $setTitleParams, true );
350
351
		// Clear the cache.
352
		$this->cache->deleteItem( $this->getEntityCacheKey( $this->id ) );
353
	}
354
355
	public function getInstanceOf() {
356
		$instancesOf = $this->getPropertyOfTypeItem( $this->getId(), self::PROP_INSTANCE_OF );
0 ignored issues
show
Unused Code introduced by
The call to Samwilson\SimpleWikidata...getPropertyOfTypeItem() has too many arguments starting with self::PROP_INSTANCE_OF. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

356
		$instancesOf = /** @scrutinizer ignore-call */ $this->getPropertyOfTypeItem( $this->getId(), self::PROP_INSTANCE_OF );

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
357
		return array_shift( $instancesOf );
358
	}
359
360
	/**
361
	 * Does this item exist?
362
	 * @return bool
363
	 */
364
	public function exists() {
365
		return $this->getId() !== false;
366
	}
367
368
	public function getWikipediaIntro() {
369
		$cacheKey = 'wikipedia-intro-' . $this->id . $this->lang;
370
		if ( $this->cache->hasItem( $cacheKey ) ) {
371
			return $this->cache->getItem( $cacheKey )->get();
372
		}
373
		$entity = $this->getEntity( $this->id );
374
		if ( !isset( $entity['sitelinks'] ) ) {
375
			return [];
376
		}
377
		foreach ( $entity['sitelinks'] as $sitelink ) {
378
			if ( $sitelink['site'] == $this->lang . 'wiki' ) {
379
				$api = new MediawikiApi( 'https://' . $this->lang . '.wikipedia.org/w/api.php' );
380
				$req = new SimpleRequest( 'query', [
381
					'prop' => 'extracts',
382
					'exintro' => true,
383
					'titles' => $sitelink['title'],
384
				] );
385
				$response = $api->getRequest( $req );
386
				$page = array_shift( $response['query']['pages'] );
387
				$out = [
388
					'title' => $page['title'],
389
					'html' => $page['extract'],
390
				];
391
				$cacheItem = $this->cache->getItem( $cacheKey )
392
					->expiresAfter( new DateInterval( 'P1D' ) )
393
					->set( $out );
394
				$this->cache->save( $cacheItem );
395
396
				return $out;
397
			}
398
		}
399
400
		return [];
401
	}
402
403
	/**
404
	 * Get the raw entity data from the 'wbgetentities' API call.
405
	 * @param string $id
406
	 * @param bool $ignoreCache
407
	 * @return bool
408
	 */
409 1
	public function getEntity( $id = null, $ignoreCache = false ) {
410 1
		$idActual = $id ?: $this->id;
411 1
		$cacheKey = $this->getEntityCacheKey( $idActual );
412 1
		if ( !$ignoreCache && $this->cache->hasItem( $cacheKey ) ) {
413
			return $this->cache->getItem( $cacheKey )->get();
414
		}
415 1
		$metadataRequest = new SimpleRequest( 'wbgetentities', [ 'ids' => $idActual ] );
416 1
		$itemResult = $this->wdApi->getRequest( $metadataRequest );
417 1
		if ( !isset( $itemResult['success'] ) || !isset( $itemResult['entities'][$id] ) ) {
418
			return false;
419
		}
420 1
		$metadata = $itemResult['entities'][$idActual];
421 1
		$cacheItem = $this->cache->getItem( $cacheKey )
422 1
			->expiresAfter( new DateInterval( 'PT10M' ) )
423 1
			->set( $metadata );
424 1
		$this->cache->save( $cacheItem );
425 1
		return $metadata;
426
	}
427
428
	/**
429
	 * @param $id
430
	 *
431
	 * @return string
432
	 */
433 1
	protected function getEntityCacheKey( $id ) {
434 1
		return 'entities' . $id;
435
	}
436
}
437