Completed
Push — master ( d2d28e...1c2760 )
by mw
35:37
created

SMWQuantityValue::initDisplayData()   B

Complexity

Conditions 6
Paths 5

Size

Total Lines 22
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 6

Importance

Changes 0
Metric Value
cc 6
eloc 13
c 0
b 0
f 0
nc 5
nop 0
dl 0
loc 22
ccs 13
cts 13
cp 1
crap 6
rs 8.6737
1
<?php
2
3
use SMW\DataValues\UnitConversionFetcher;
4
use SMW\Message;
5
use SMW\ApplicationFactory;
6
7
/**
8
 * @ingroup SMWDataValues
9
 */
10
11
/**
12
 * This datavalue implements unit support custom units, for which users have
13
 * provided linear conversion factors within the wiki. Those user settings
14
 * are retrieved from a property page associated with this object.
15
 *
16
 * @author Markus Krötzsch
17
 * @ingroup SMWDataValues
18
 */
19
class SMWQuantityValue extends SMWNumberValue {
0 ignored issues
show
Coding Style Compatibility introduced by
PSR1 recommends that each class must be in a namespace of at least one level to avoid collisions.

You can fix this by adding a namespace to your class:

namespace YourVendor;

class YourClass { }

When choosing a vendor namespace, try to pick something that is not too generic to avoid conflicts with other libraries.

Loading history...
20
21
	/**
22
	 * Array with format (canonical unit ID string) => (conversion factor)
23
	 * @var float[]|bool
24
	 */
25
	protected $m_unitfactors = false;
26
27
	/**
28
	 * Array with format (normalised unit string) => (canonical unit ID string)
29
	 * @var string[]|bool
30
	 */
31
	protected $m_unitids = false;
32
33
	/**
34
	 * Ordered array of (normalized) units that should be displayed in tooltips, etc.
35
	 * @var string[]|bool
36
	 */
37
	protected $m_displayunits = false;
38
39
	/**
40
	 * Main unit in canonical form (recognised by the conversion factor 1)
41
	 * @var string|bool
42
	 */
43
	protected $m_mainunit = false;
44
45 22
	protected function convertToMainUnit( $number, $unit ) {
46 22
		$this->initConversionData();
47
48 22
		if ( array_key_exists( $unit, $this->m_unitids ) ) {
49 21
			$this->m_unitin = $this->m_unitids[$unit];
50 21
			assert( $this->m_unitfactors[$this->m_unitin] != 0 /* Should be filtered by initConversionData() */ );
51 21
			$this->m_dataitem = new SMWDINumber( $number / $this->m_unitfactors[$this->m_unitin], $this->m_typeid );
0 ignored issues
show
Unused Code introduced by
The call to SMWDINumber::__construct() has too many arguments starting with $this->m_typeid.

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.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
52 21
			return true;
53
		} else { // unsupported unit
54 7
			return false;
55
		}
56
	}
57
58 13
	protected function makeConversionValues() {
59 13
		if ( $this->m_unitvalues !== false ) {
60
			return; // do this only once
61
		}
62
63 13
		$this->m_unitvalues = array();
64
65 13
		if ( !$this->isValid() ) {
66
			return;
67
		}
68
69 13
		$this->initDisplayData();
70
71 13
		if ( count( $this->m_displayunits ) == 0 ) { // no display units, just show all
72 11
			foreach ( $this->m_unitfactors as $unit => $factor ) {
0 ignored issues
show
Bug introduced by
The expression $this->m_unitfactors of type array<integer,double>|boolean 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...
73 11
				if ( $unit !== '' ) { // filter out the empty fallback unit that is always there
74 11
					$this->m_unitvalues[$unit] = $this->m_dataitem->getNumber() * $factor;
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 getNumber() does only exist in the following sub-classes of SMWDataItem: SMWDINumber. 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...
75
				}
76
			}
77
		} else {
78 3
			foreach ( $this->m_displayunits as $unit ) {
0 ignored issues
show
Bug introduced by
The expression $this->m_displayunits of type array<integer,string>|boolean 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...
79
				/// NOTE We keep non-ID units unless the input unit is used, so display units can be used to pick
80
				/// the preferred form of a unit. Doing this requires us to recompute the conversion values whenever
81
				/// the m_unitin changes.
82 3
				$unitkey = ( $this->m_unitids[$unit] == $this->m_unitin ) ? $this->m_unitids[$unit] : $unit;
83 3
				$this->m_unitvalues[$unitkey] = $this->m_dataitem->getNumber() * $this->m_unitfactors[$this->m_unitids[$unit]];
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 getNumber() does only exist in the following sub-classes of SMWDataItem: SMWDINumber. 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...
84
			}
85
		}
86 13
	}
87
88 8
	protected function makeUserValue() {
89 8
		$printunit = false; // the normalised string of a known unit to use for printouts
90
91
		// Check if a known unit is given as outputformat:
92 8
		if ( ( $this->m_outformat ) && ( $this->m_outformat != '-' ) &&
93 8
		     ( $this->m_outformat != '-n' ) && ( $this->m_outformat != '-u' ) ) { // first try given output unit
94 2
			$wantedunit = $this->normalizeUnit( $this->m_outformat );
95 2
			if ( array_key_exists( $wantedunit, $this->m_unitids ) ) {
96 2
				$printunit = $wantedunit;
97
			}
98
		}
99
100
		// Alternatively, try to use the main display unit as a default:
101 8
		if ( $printunit === false ) {
102 8
			$this->initDisplayData();
103 8
			if ( count( $this->m_displayunits ) > 0 ) {
104 3
				$printunit = reset( $this->m_displayunits );
105
			}
106
		}
107
		// Finally, fall back to main unit:
108 8
		if ( $printunit === false ) {
109 6
			$printunit = $this->getUnit();
110
		}
111
112 8
		$asPrefix = isset( $this->prefixalUnitPreference[$printunit] ) && $this->prefixalUnitPreference[$printunit];
113
114 8
		$this->m_unitin = $this->m_unitids[$printunit];
115 8
		$this->m_unitvalues = false; // this array depends on m_unitin if displayunits were used, better invalidate it here
0 ignored issues
show
Documentation Bug introduced by
It seems like false of type false is incompatible with the declared type array of property $m_unitvalues.

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...
116
117 8
		$value = $this->m_dataitem->getNumber() * $this->m_unitfactors[$this->m_unitin];
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 getNumber() does only exist in the following sub-classes of SMWDataItem: SMWDINumber. 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...
118
119 8
		$this->m_caption = '';
120
121 8
		if ( $this->m_outformat != '-u' ) { // -u is the format for displaying the unit only
122 8
			$this->m_caption .= ( ( $this->m_outformat != '-' ) && ( $this->m_outformat != '-n' ) ? $this->getLocalizedFormattedNumber( $value ) : $this->getNormalizedFormattedNumber( $value ) );
123
		}
124
125 8
		if ( ( $printunit !== '' ) && ( $this->m_outformat != '-n' ) ) { // -n is the format for displaying the number only
126
127 8
			$sep = '';
128
129 8
			if ( $this->m_outformat != '-u' ) {
130 8
				$sep =  ( $this->m_outformat != '-' ? '&#160;' : ' ' );
131
			}
132
133 8
			$this->m_caption = $asPrefix ? $printunit . $sep . $this->m_caption : $this->m_caption . $sep . $printunit;
134
		}
135 8
	}
136
137
	public function getUnitList() {
138
		$this->initConversionData();
139
		return array_keys( $this->m_unitfactors );
140
	}
141
142 7
	public function getUnit() {
143 7
		$this->initConversionData();
144 7
		return $this->m_mainunit;
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->m_mainunit; of type string|boolean adds the type boolean to the return on line 144 which is incompatible with the return type of the parent method SMWNumberValue::getUnit of type string.
Loading history...
145
	}
146
147
/// The remaining functions are relatively "private" but are kept protected since
148
/// subclasses might exploit this to, e.g., "fake" conversion factors instead of
149
/// getting them from the database. A cheap way of making built-in types.
150
151
	/**
152
	 * This method initializes $m_unitfactors, $m_unitids, and $m_mainunit.
153
	 */
154 22
	protected function initConversionData() {
155 22
		if ( $this->m_unitids !== false ) {
156 14
			return; // do the below only once
157
		}
158
159 22
		$unitConversionFetcher = new UnitConversionFetcher( $this );
160 22
		$unitConversionFetcher->fetchCachedConversionData( $this->m_property );
161
162 22
		if ( $unitConversionFetcher->getErrors() !== array() ) {
163 7
			foreach ( $unitConversionFetcher->getErrors() as $error ) {
164 7
				$this->addErrorMsg(
165
					$error,
166 7
					Message::TEXT,
167 7
					Message::USER_LANGUAGE
0 ignored issues
show
Unused Code introduced by
The call to SMWQuantityValue::addErrorMsg() has too many arguments starting with \SMW\Message::USER_LANGUAGE.

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.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
168
				);
169
			}
170
		}
171
172 22
		$this->m_unitids = $unitConversionFetcher->getUnitIds();
173 22
		$this->m_unitfactors = $unitConversionFetcher->getUnitFactors();
174 22
		$this->m_mainunit = $unitConversionFetcher->getMainUnit();
175 22
		$this->prefixalUnitPreference = $unitConversionFetcher->getPrefixalUnitPreference();
176 22
	}
177
178
	/**
179
	 * This method initializes $m_displayunits.
180
	 */
181 14
	protected function initDisplayData() {
182 14
		if ( $this->m_displayunits !== false ) {
183 6
			return; // do the below only once
184
		}
185 14
		$this->initConversionData(); // needed to normalise unit strings
186 14
		$this->m_displayunits = array();
187
188 14
		if ( is_null( $this->m_property ) || is_null( $this->m_property->getDIWikiPage() ) ) {
189
			return;
190
		}
191
192 14
		$units = ApplicationFactory::getInstance()->getPropertySpecificationLookup()->getDisplayUnitsBy(
193 14
			$this->getProperty()
0 ignored issues
show
Bug introduced by
It seems like $this->getProperty() can be null; however, getDisplayUnitsBy() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
194
		);
195
196 14
		foreach ( $units as $unit ) {
197 4
			$unit = $this->normalizeUnit( $unit );
198 4
			if ( array_key_exists( $unit, $this->m_unitids ) ) {
199 4
				$this->m_displayunits[] = $unit; // do not avoid duplicates, users can handle this
200
			} // note: we ignore unsuppported units -- no way to display them
201
		}
202 14
	}
203
}
204