Passed
Push — master ( fe0bb9...12c67d )
by Sam
02:20
created

Item   F

Complexity

Total Complexity 64

Size/Duplication

Total Lines 433
Duplicated Lines 8.31 %

Test Coverage

Coverage 25.12%

Importance

Changes 0
Metric Value
dl 36
loc 433
ccs 52
cts 207
cp 0.2512
rs 3.4883
c 0
b 0
f 0
wmc 64

20 Methods

Rating   Name   Duplication   Size   Complexity  
A getLabel() 0 8 2
A getWikidataUrl() 0 2 1
A __construct() 0 9 3
A setCache() 0 2 1
A factory() 0 15 3
A getId() 0 3 2
A getPropertyOfTypeUrl() 0 11 3
C getPropertyOfTypeExternalIdentifier() 0 28 7
B getWikipediaIntro() 0 33 5
B setPropertyOfTypeText() 0 36 5
A getEntityCacheKey() 0 2 1
B setPropertyOfTypeItem() 0 37 4
A exists() 0 2 1
B getStandardProperties() 0 29 4
A getInstanceOf() 0 3 1
B getPropertyOfTypeText() 0 17 6
A getPropertyOfTypeQuantity() 0 13 3
A getPropertyOfTypeItem() 11 11 3
A getPropertyOfTypeTime() 23 23 3
B getEntity() 0 17 6

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Item often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Item, and based on these observations, apply Extract Interface, too.

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
	/**
104
	 * Wikiprojects list their properties like this:
105
	 *
106
	 *     {{List of properties/Header}}
107
	 *     {{List of properties/Row|id=31|example-subject=Q923767|example-object=Q3331189}}
108
	 *     </table>
109
	 *
110
	 * @param string $wikiProject
111
	 * @param string $type
112
	 * @return array
113
	 */
114
	public function getStandardProperties( $wikiProject = 'WikiProject_Books', $type = 'work' ) {
115
		if ( $type !== 'work' ) {
116
			$type = 'edition';
117
		}
118
		$cacheKey = $type . '_item_property_IDs';
119
		if ( $this->cache->hasItem( $cacheKey ) ) {
120
			$propIds = $this->cache->getItem( $cacheKey )->get();
121
		} else {
122
			$domCrawler = new Crawler();
123
			$wikiProjectUrl = 'https://www.wikidata.org/wiki/Wikidata:' . $wikiProject;
124
			$domCrawler->addHtmlContent( file_get_contents( $wikiProjectUrl ) );
125
			$propAncors = "//h3/span[@id='" . ucfirst( $type ) . "_item_properties']/../following-sibling::table[1]//td[2]/a";
126
			$propCells = $domCrawler->filterXPath( $propAncors );
127
			$propIds = [];
128
			$propCells->each( function ( Crawler $node, $i ) use ( &$propIds ) {
129
				$propId = $node->text();
130
				$propIds[] = $propId;
131
			} );
132
			$cacheItem = $this->cache->getItem( $cacheKey )
133
				->expiresAfter( new DateInterval( 'PT1H' ) )
134
				->set( $propIds );
135
			$this->cache->save( $cacheItem );
136
		}
137
		$workProperties = [];
138
		foreach ( $propIds as $propId ) {
139
			$workProperties[] = self::factory( $propId, $this->lang, $this->cache );
140
		}
141
142
		return $workProperties;
143
	}
144
145
	/**
146
	 * @param string $propertyId
147
	 * @return bool|Time[]
148
	 */
149 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...
150
		$times = [];
151
		$entity = $this->getEntity();
152
		if ( !isset( $entity['claims'][$propertyId] ) ) {
153
			// No statements for this property.
154
			return $times;
155
		}
156
		// 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...
157
		foreach ( $entity['claims'][$propertyId] as $claim ) {
158
			// 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...
159
			$times[] = new Time( $claim, $this->lang, $this->cache );
160
//
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...
161
// $timeValue = $claim['datavalue']['value']['time'];
162
// // Ugly workaround for imprecise dates. :-(
163
// if (preg_match('/([0-9]{1,4})-00-00/', $timeValue, $matches) === 1) {
164
// $timeValue = $matches[1];
165
// return $timeValue;
166
// }
167
// $time = strtotime($timeValue);
168
// return date($dateFormat, $time);
169
			// }
170
		}
171
		return $times;
172
	}
173
174
	/**
175
	 * Get the Item that is referred to by the specified item's property.
176
	 *
177
	 * @param string $propertyId
178
	 *
179
	 * @return \Samwilson\SimpleWikidata\Properties\Item[]
180
	 */
181 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...
182 1
		$entity = $this->getEntity( $this->id );
183 1
		if ( !isset( $entity['claims'][$propertyId] ) ) {
184 1
			return [];
185
		}
186 1
		$items = [];
187 1
		foreach ( $entity['claims'][$propertyId] as $claim ) {
188 1
			$items[] = new Properties\Item( $claim, $this->lang, $this->cache );
189
		}
190
191 1
		return $items;
192
	}
193
194
	public function setPropertyOfTypeItem( $property, $itemId ) {
195
		$itemIdNumeric = substr( $itemId, 1 );
196
197
		// First see if this property already exists, and that it is different from what's being set.
198
		$entity = $this->getEntity( $this->id );
199
		if ( !empty( $entity['claims'][$property] ) ) {
200
			// Get the first claim, and update it if necessary.
201
			$claim = array_shift( $entity['claims'][$property] );
202
			if ( $claim['mainsnak']['datavalue']['value']['id'] == $itemId ) {
203
				// Already is the required value, no need to change.
204
				return;
205
			}
206
			$claim['mainsnak']['datavalue']['value']['id'] = $itemId;
207
			$claim['mainsnak']['datavalue']['value']['numeric-id'] = $itemIdNumeric;
208
			$apiParams = [
209
				'action' => 'wbsetclaim',
210
				'claim' => json_encode( $claim ),
211
			];
212
		}
213
214
		// If no claim was found (and modified) above, create a new claim.
215
		if ( !isset( $apiParams ) ) {
216
			$apiParams = [
217
				'action' => 'wbcreateclaim',
218
				'entity' => $this->getId(),
219
				'property' => $property,
220
				'snaktype' => 'value',
221
				'value' => json_encode( [ 'entity-type' => 'item', 'numeric-id' => $itemIdNumeric ] ),
222
			];
223
		}
224
225
		// Save the property.
226
		$wdWpOauth = new WdOauth();
0 ignored issues
show
Bug introduced by
The type Samwilson\SimpleWikidata\WdOauth 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...
227
		$wdWpOauth->makeCall( $apiParams, true );
228
229
		// Clear the cache.
230
		$this->cache->deleteItem( $this->getEntityCacheKey( $this->id ) );
231
	}
232
233
	public function getPropertyOfTypeUrl( $entityId, $propertyId ) {
234
		$entity = $this->getEntity( $entityId );
235
		if ( !isset( $entity['claims'][$propertyId] ) ) {
236
			return false;
237
		}
238
		$urls = [];
239
		foreach ( $entity['claims'][$propertyId] as $claim ) {
240
			$urls[] = $claim['mainsnak']['datavalue']['value'];
241
		}
242
243
		return $urls;
244
	}
245
246
	public function getPropertyOfTypeExternalIdentifier( $entityId, $propertyId ) {
247
		$entity = $this->getEntity( $entityId );
248
		if ( !isset( $entity['claims'][$propertyId] ) ) {
249
			return false;
250
		}
251
		$idents = [];
252
		foreach ( $entity['claims'][$propertyId] as $claim ) {
253
			$qualifiers = [];
254
			if ( !isset( $claim['qualifiers'] ) ) {
255
				continue;
256
			}
257
			foreach ( $claim['qualifiers'] as $qualsInfo ) {
258
				foreach ( $qualsInfo as $qualInfo ) {
259
					$qualProp = self::factory( $qualInfo['property'], $this->lang, $this->cache );
260
					$propLabel = $qualProp->getLabel();
261
					if ( !isset( $qualifiers[$propLabel] ) ) {
262
						$qualifiers[$propLabel] = [];
263
					}
264
					$qualifiers[$propLabel][] = $qualInfo['datavalue']['value'];
265
				}
266
			}
267
			$idents[] = [
268
				'qualifiers' => $qualifiers,
269
				'value' => $claim['mainsnak']['datavalue']['value'],
270
			];
271
		}
272
273
		return $idents;
274
	}
275
276
	/**
277
	 * Get a single-valued text property.
278
	 * @param string $property One of the PROP_* constants.
279
	 * @return string|bool The value, or false if it can't be found.
280
	 */
281
	public function getPropertyOfTypeText( $property ) {
282
		$entity = $this->getEntity( $this->id );
283
		if ( isset( $entity['claims'][$property] ) ) {
284
			// Use the first title.
285
			foreach ( $entity['claims'][$property] as $t ) {
286
				if ( !isset( $t['mainsnak']['datavalue']['value']['language'] ) ) {
287
					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...
288
					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...
289
				}
290
				if ( $t['mainsnak']['datavalue']['value']['language'] == $this->lang
291
					&& !empty( $t['mainsnak']['datavalue']['value']['text'] )
292
				) {
293
					return $t['mainsnak']['datavalue']['value']['text'];
294
				}
295
			}
296
		}
297
		return false;
298
	}
299
300
	/**
301
	 * 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.
302
	 *   - amount – implicit part of the string (mapping of unit prefix is unclear)
303
	 *   - unit – implicit part of the string that defaults to "1" (mapping to standardizing body is unclear)
304
	 *   - upperbound - quantity's upper bound
305
	 *   - lowerbound - quantity's lower bound
306
	 * @param $property
307
	 * @return mixed[]|bool If it's not false it's an array with 'amount', 'unit', etc.
308
	 */
309
	public function getPropertyOfTypeQuantity( $property ) {
310
		$quantities = [];
311
		$entity = $this->getEntity( $this->id );
312
		if ( !isset( $entity['claims'][$property] ) ) {
313
			return false;
314
		}
315
		foreach ( $entity['claims'][$property] as $t ) {
316
			$quantity = $t['mainsnak']['datavalue']['value'];
317
			$unitId = substr( $quantity['unit'], strlen( $this->wikidataUrlBase ) + 1 );
318
			$quantity['unit'] = self::factory( $unitId, $this->lang, $this->cache );
319
			$quantities[] = $quantity;
320
		}
321
		return $quantities;
322
	}
323
324
	/**
325
	 * Set a single-valued text property.
326
	 * @param string $property One of the PROP_* constants.
327
	 * @param string $value The value.
328
	 */
329
	public function setPropertyOfTypeText( $property, $value ) {
330
		// First see if this property already exists, and that it is different from what's being set.
331
		$entity = $this->getEntity( $this->id );
332
		if ( !empty( $entity['claims'][$property] ) ) {
333
			// Find this language's claim (if there is one).
334
			foreach ( $entity['claims'][$property] as $claim ) {
335
				if ( $claim['mainsnak']['datavalue']['value']['language'] == $this->lang ) {
336
					// Modify this claim's text value.
337
					$titleClaim = $claim;
338
					$titleClaim['mainsnak']['datavalue']['value']['text'] = $value;
339
					$setTitleParams = [
340
						'action' => 'wbsetclaim',
341
						'claim' => \GuzzleHttp\json_encode( $titleClaim ),
342
					];
343
					continue;
344
				}
345
			}
346
		}
347
348
		// If no claim was found (and modified) above, create a new claim.
349
		if ( !isset( $setTitleParams ) ) {
350
			$setTitleParams = [
351
				'action' => 'wbcreateclaim',
352
				'entity' => $this->getId(),
353
				'property' => $property,
354
				'snaktype' => 'value',
355
				'value' => \GuzzleHttp\json_encode( [ 'text' => $value, 'language' => $this->lang ] ),
356
			];
357
		}
358
359
		// Save the property.
360
		$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...
361
		$wdWpOauth->makeCall( $setTitleParams, true );
362
363
		// Clear the cache.
364
		$this->cache->deleteItem( $this->getEntityCacheKey( $this->id ) );
365
	}
366
367
	public function getInstanceOf() {
368
		$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

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