Completed
Push — master ( 7776a4...8c5813 )
by mw
35:00
created

MonolingualTextValue   B

Complexity

Total Complexity 42

Size/Duplication

Total Lines 280
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 14

Test Coverage

Coverage 83.64%

Importance

Changes 3
Bugs 0 Features 3
Metric Value
wmc 42
lcom 2
cbo 14
dl 0
loc 280
ccs 92
cts 110
cp 0.8364
rs 8.295
c 3
b 0
f 3

16 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
A setFieldProperties() 0 4 1
A needsLanguageCode() 0 3 1
C parseUserValue() 0 43 12
A getValuesFromString() 0 3 1
A loadDataItem() 0 14 3
A getShortWikiText() 0 3 1
A getShortHTMLText() 0 3 1
A getLongWikiText() 0 3 1
A getLongHTMLText() 0 3 1
A getWikiValue() 0 3 1
A getPropertyDataItems() 0 12 4
A getDataItems() 0 21 4
B getTextValueByLanguage() 0 29 6
A newContainerSemanticData() 0 20 2
A newLanguageCodeValue() 0 12 2

How to fix   Complexity   

Complex Class

Complex classes like MonolingualTextValue 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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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 MonolingualTextValue, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace SMW\DataValues;
4
5
use SMWDataValue as DataValue;
6
use SMW\DIProperty;
7
use SMW\Localizer;
8
use SMW\DIWikiPage;
9
use SMW\DataValueFactory;
10
use SMW\ApplicationFactory;
11
use SMWContainerSemanticData as ContainerSemanticData;
12
use SMWDIContainer as DIContainer;
13
use SMWDataItem as DataItem;
14
use SMW\DataValues\ValueFormatters\DataValueFormatter;
15
16
/**
17
 * MonolingualTextValue requires two components, a language code and a
18
 * text.
19
 *
20
 * A text `foo@en` is expected to be invoked with a BCP47 language
21
 * code tag and a language dependent text component.
22
 *
23
 * Internally, the value is stored as container object that represents
24
 * the language code and text as separate entities in order to be queried
25
 * individually.
26
 *
27
 * External output representation depends on the context (wiki, html)
28
 * whether the language code is omitted or not.
29
 *
30
 * @license GNU GPL v2+
31
 * @since 2.4
32
 *
33
 * @author mwjames
34
 */
35
class MonolingualTextValue extends DataValue {
36
37
	/**
38
	 * @var DIProperty[]|null
39
	 */
40
	private static $properties = null;
41
42
	/**
43
	 * @var MonolingualTextValueParser
44
	 */
45
	private $monolingualTextValueParser = null;
46
47
	/**
48
	 * @param string $typeid
49 12
	 */
50 12
	public function __construct( $typeid = '' ) {
51 12
		parent::__construct( '_mlt_rec' );
52 12
		$this->monolingualTextValueParser = ValueParserFactory::getInstance()->newMonolingualTextValueParser();
0 ignored issues
show
Documentation Bug introduced by
It seems like \SMW\DataValues\ValuePar...ingualTextValueParser() of type object<SMW\DataValues\Va...lingualTextValueParser> is incompatible with the declared type object<SMW\DataValues\MonolingualTextValueParser> of property $monolingualTextValueParser.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
53
	}
54
55
	/**
56
	 * @see RecordValue::setFieldProperties
57
	 *
58
	 * @param SMWDIProperty[] $properties
59
	 */
60
	public function setFieldProperties( array $properties ) {
0 ignored issues
show
Unused Code introduced by
The parameter $properties is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
61
		// Keep the interface, but the properties for this type
62
		// are fixed.
63
	}
64
65
	/**
66
	 * @since 2.4
67
	 *
68
	 * @return integer
69 2
	 */
70 2
	public function needsLanguageCode() {
71
		return ( $this->getOptionValueFor( 'smwgDVFeatures' ) & SMW_DV_MLTV_LCODE ) != 0;
72
	}
73
74
	/**
75
	 * @see DataValue::parseUserValue
76
	 * @note called by DataValue::setUserValue
77
	 *
78
	 * @param string $value
0 ignored issues
show
Bug introduced by
There is no parameter named $value. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
79 10
	 */
80
	protected function parseUserValue( $userValue ) {
81 10
82
		list( $text, $languageCode ) = $this->getValuesFromString( $userValue );
83 10
84
		$languageCodeValue = $this->newLanguageCodeValue( $languageCode );
85
86 10
		if (
87 10
			( $languageCode !== '' && $languageCodeValue->getErrors() !== array() ) ||
88 3
			( $languageCode === '' && $this->needsLanguageCode() ) ) {
89 3
			$this->addError( $languageCodeValue->getErrors() );
90
			return;
91
		}
92 7
93
		$dataValues = array();
94 7
95
		foreach ( $this->getPropertyDataItems() as $property ) {
0 ignored issues
show
Bug introduced by
The expression $this->getPropertyDataItems() of type array<integer,object<SMW\DIProperty>>|null is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
96
97 7
			if (
98 7
				( $languageCode === '' && $property->getKey() === '_LCODE' ) ||
99 1
				( $text === '' && $property->getKey() === '_TEXT' ) ) {
100
				continue;
101
			}
102 7
103 7
			$dataValue = DataValueFactory::getInstance()->newPropertyObjectValue(
104 7
				$property,
105 7
				$property->getKey() === '_LCODE' ? $languageCode : $text,
106 7
				false,
107 7
				$this->m_contextPage
108
			);
109 7
110 7
			$dataValues[] = $dataValue;
111
		}
112
113
		// Generate a hash from the normalized representation so that foo@en being
114 7
		// the same as foo@EN independent of a user input
115
		$containerSemanticData = $this->newContainerSemanticData( $text . '@' . $languageCode );
116 7
117 7
		foreach ( $dataValues as $dataValue ) {
118 7
			$containerSemanticData->addDataValue( $dataValue );
119
		}
120 7
121 7
		$this->m_dataitem = new DIContainer( $containerSemanticData );
122
	}
123
124
	/**
125
	 * @note called by MonolingualTextValueDescriptionDeserializer::deserialize
126
	 * and MonolingualTextValue::parseUserValue
127
	 *
128
	 * No explicit check is made on the validity of a language code and is
129
	 * expected to be done before calling this method.
130
	 *
131
	 * @since 2.4
132
	 *
133
	 * @param string $userValue
134
	 *
135
	 * @return array
136 10
	 */
137 10
	public function getValuesFromString( $userValue ) {
138
		return $this->monolingualTextValueParser->parse( $userValue );
139
	}
140
141
	/**
142
	 * @see DataValue::loadDataItem
143
	 *
144
	 * @param DataItem $dataItem
145
	 *
146
	 * @return boolean
147 3
	 */
148
	protected function loadDataItem( DataItem $dataItem ) {
149 3
150
		if ( $dataItem->getDIType() === DataItem::TYPE_CONTAINER ) {
151
			$this->m_dataitem = $dataItem;
152 3
			return true;
153 3
		} elseif ( $dataItem->getDIType() === DataItem::TYPE_WIKIPAGE ) {
154 3
			$semanticData = new ContainerSemanticData( $dataItem );
0 ignored issues
show
Compatibility introduced by
$dataItem of type object<SMWDataItem> is not a sub-type of object<SMW\DIWikiPage>. It seems like you assume a child class of the class SMWDataItem to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
155 3
			$semanticData->copyDataFrom( ApplicationFactory::getInstance()->getStore()->getSemanticData( $dataItem ) );
0 ignored issues
show
Compatibility introduced by
$dataItem of type object<SMWDataItem> is not a sub-type of object<SMW\DIWikiPage>. It seems like you assume a child class of the class SMWDataItem to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
156 3
			$this->m_dataitem = new DIContainer( $semanticData );
157
			return true;
158
		}
159 1
160
		return false;
161
	}
162
163
	/**
164
	 * @see DataValue::getShortWikiText
165 3
	 */
166
	public function getShortWikiText( $linker = null ) {
167 3
		return $this->getDataValueFormatter()->format( DataValueFormatter::WIKI_SHORT, $linker );
168
	}
169
170
	/**
171 3
	 * @see DataValue::getShortHTMLText
172
	 */
173
	public function getShortHTMLText( $linker = null ) {
174
		return $this->getDataValueFormatter()->format( DataValueFormatter::HTML_SHORT, $linker );
175
	}
176
177
	/**
178
	 * @see DataValue::getLongWikiText
179
	 */
180
	public function getLongWikiText( $linker = null ) {
181
		return $this->getDataValueFormatter()->format( DataValueFormatter::WIKI_LONG, $linker );
182
	}
183
184
	/**
185
	 * @see DataValue::getLongHTMLText
186
	 */
187
	public function getLongHTMLText( $linker = null ) {
188
		return $this->getDataValueFormatter()->format( DataValueFormatter::HTML_LONG, $linker );
189
	}
190
191
	/**
192
	 * @see DataValue::getWikiValue
193
	 */
194
	public function getWikiValue() {
195
		return $this->getDataValueFormatter()->format( DataValueFormatter::VALUE );
196
	}
197
198
	/**
199
	 * @since 2.4
200
	 * @note called by SMWResultArray::getNextDataValue
201
	 *
202
	 * @return DIProperty[]
203 4
	 */
204 4
	public static function getPropertyDataItems() {
205
206
		if ( self::$properties !== null && self::$properties !== array() ) {
207
			return self::$properties;
208
		}
209
210
		foreach ( array( '_TEXT', '_LCODE' ) as  $id ) {
211
			self::$properties[] = new DIProperty( $id );
212
		}
213 8
214
		return self::$properties;
215 8
	}
216 7
217
	/**
218
	 * @since 2.4
219 1
	 * @note called by SMWResultArray::loadContent
220 1
	 *
221 1
	 * @return DataItem[]
222
	 */
223 1
	public function getDataItems() {
224
225
		if ( !$this->isValid() ) {
226
			return array();
227
		}
228
229
		$result = array();
230
		$index = 0;
231
232 2
		foreach ( $this->getPropertyDataItems() as $diProperty ) {
0 ignored issues
show
Bug introduced by
The expression $this->getPropertyDataItems() of type array<integer,object<SMW\DIProperty>>|null is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
233
			$values = $this->getDataItem()->getSemanticData()->getPropertyValues( $diProperty );
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class SMWDataItem as the method getSemanticData() does only exist in the following sub-classes of SMWDataItem: SMWDIContainer. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
234 2
			if ( count( $values ) > 0 ) {
235
				$result[$index] = reset( $values );
236
			} else {
237
				$result[$index] = null;
238 2
			}
239 2
			$index += 1;
240
		}
241 2
242 2
		return $result;
243 2
	}
244 2
245 2
	/**
246
	 * @since 2.4
247
	 *
248 2
	 * @return DataValue|null
249 2
	 */
250
	public function getTextValueByLanguage( $languageCode ) {
251 2
252
		if ( !$this->isValid() || $this->getDataItem() === array() ) {
253
			return null;
254
		}
255
256
		$semanticData = $this->getDataItem()->getSemanticData();
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class SMWDataItem as the method getSemanticData() does only exist in the following sub-classes of SMWDataItem: SMWDIContainer. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
257
258
		$dataItems = $semanticData->getPropertyValues( new DIProperty( '_LCODE' ) );
259 3
		$dataItem = reset( $dataItems );
260
261 3
		if ( $dataItem === false || ( $dataItem->getString() !== Localizer::asBCP47FormattedLanguageCode( $languageCode ) ) ) {
262
			return null;
263
		}
264
265 3
		$dataItems = $semanticData->getPropertyValues( new DIProperty( '_TEXT' ) );
266
		$dataItem = reset( $dataItems );
267 3
268 3
		if ( $dataItem === false ) {
269
			return null;
270 3
		}
271 1
272
		$dataValue = DataValueFactory::getInstance()->newDataItemValue(
273
			$dataItem,
274 2
			new DIProperty( '_TEXT' )
275 2
		);
276
277 2
		return $dataValue;
278
	}
279
280
	private function newContainerSemanticData( $value ) {
281 2
282 2
		if ( $this->m_contextPage === null ) {
283 2
			$containerSemanticData = ContainerSemanticData::makeAnonymousContainer();
284 2
			$containerSemanticData->skipAnonymousCheck();
285
		} else {
286 2
			$subobjectName = '_ML' . md5( $value );
287
288
			$subject = new DIWikiPage(
289 5
				$this->m_contextPage->getDBkey(),
290
				$this->m_contextPage->getNamespace(),
291 5
				$this->m_contextPage->getInterwiki(),
292 1
				$subobjectName
293
			);
294
295
			$containerSemanticData = new ContainerSemanticData( $subject );
296
		}
297 4
298
		return $containerSemanticData;
299 1
	}
300 1
301
	private function newLanguageCodeValue( $languageCode ) {
302 1
303
		$languageCodeValue = new LanguageCodeValue();
304
305
		if ( $this->m_property !== null ) {
306 1
			$languageCodeValue->setProperty( $this->m_property );
307
		}
308
309 4
		$languageCodeValue->setUserValue( $languageCode );
310
311
		return $languageCodeValue;
312 4
	}
313
314
}
315