Passed
Branch master (27f6d9)
by Michael
04:19
created

VoteHandler::checkCandidates()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 13
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
dl 0
loc 13
ccs 0
cts 6
cp 0
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 6
nc 3
nop 1
crap 12
1
<?php
2
3
namespace Michaelc\Voting\STV;
4
5
use Michaelc\Voting\STV\{Election, Ballot, Candidate};
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Michaelc\Voting\STV\Election.

Let’s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let’s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
Bug introduced by
This use statement conflicts with another class in this namespace, Michaelc\Voting\STV\Ballot.

Let’s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let’s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
Bug introduced by
This use statement conflicts with another class in this namespace, Michaelc\Voting\STV\Candidate.

Let’s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let’s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
6
7
class VoteHandler
8
{
9
    /**
10
     * Election object
11
     *
12
     * @var \Michaelc\Voting\STV\Election;
13
     */
14
    protected $election;
15
16
    /**
17
     * Array of all ballots in election
18
     *
19
     * @var \MichaelC\Voting\STV\Ballot[]
20
     */
21
    protected $ballots;
22
23
    /**
24
     * Quota of votes needed for a candidate to be elected
25
     *
26
     * @var int
27
     */
28
    protected $quota;
29
30
    /**
31
     * Number of candidates elected so far
32
     *
33
     * @var int
34
     */
35
    protected $electedCandidates;
36
37
    /**
38
     * Constructor
39
     *
40
     * @param Election $election
41
     */
42 1
    public function __construct(Election $election)
43
    {
44 1
        $this->election = $election;
45 1
        $this->ballots = $this->election->getBallots();
46 1
        $this->quota = $this->getQuota();
47 1
    }
48
49
    /**
50
     * Run the election
51
     *
52
     * @return \MichaelC\Voting\STV\Candidate[]	Winning candidates
53
     */
54
    public function run()
55
    {
56
    	$this->firstStep();
57
58
        $candidates = $this->election->getActiveCandidates();
59
60
        while ($electedCandidates < $this->election->getWinners())
0 ignored issues
show
Bug introduced by
The variable $electedCandidates does not exist. Did you mean $candidates?

This check looks for variables that are accessed but have not been defined. It raises an issue if it finds another variable that has a similar name.

The variable may have been renamed without also renaming all references.

Loading history...
Bug introduced by
The method getWinners() does not exist on Michaelc\Voting\STV\Election. Did you maybe mean getWinnersCount()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
61
        {
62
	    	if (!$this->checkCandidates($candidates))
63
	    	{
64
	    		$this->eliminateCandidates($candidates);
65
	    	}
66
        }
67
68
        return $this->election->getElectedCandidates();
69
    }
70
71
    /**
72
     * Perform the initial vote allocation
73
     *
74
     * @return
75
     */
76
    public function firstStep()
77
    {
78
        foreach ($this->ballots as $i => $ballot)
79
        {
80
            $this->allocateVotes($ballot);
81
        }
82
83
        return;
84
    }
85
86
    /**
87
     * Check if any candidates have reached the quota and can be elected
88
     *
89
     * @param  array  $candidates 	Array of active candidates to check
90
     * @return bool 				Whether any candidates were changed to elected
91
     */
92
    protected function checkCandidates(array &$candidates): bool
93
    {
94
    	foreach ($candidates as $i => $candidate)
95
        {
96
            if ($candidate->getVotes() >= $this->quota)
97
            {
98
                $this->electCandidate($candidate);
99
                $elected = true;
100
            }
101
        }
102
103
        return $elected ?? false;
0 ignored issues
show
Bug introduced by
The variable $elected does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
104
    }
105
106
    /**
107
     * Allocate the next votes from a Ballot
108
     *
109
     * @param Ballot $ballot 		The ballot to allocate the votes from
110
     * @param float  $multiplier 	Number to multiply the weight by (surplus)
111
     * @param float  $divisor 		The divisor of the weight (Total number of
112
     *                          	candidate votes)
113
     * @return Ballot 	The same ballot passed in modified
114
     */
115
    protected function allocateVotes(Ballot &$ballot, float $multiplier = 1.0, float $divisor = 1.0): Ballot
116
    {
117
        $weight = $ballot->setWeight(($ballot->getWeight() * $multiplier) / $divisor);
118
        $candidate = $ballot->getNextChoice();
119
120
        if ($candidate !== null)
121
        {
122
	        $this->election->getCandidate($candidate->getId())->addVotes($weight);
0 ignored issues
show
Bug introduced by
The method getId cannot be called on $candidate (of type integer).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
123
	        $ballot->setLevelUsed(($step - 1));
0 ignored issues
show
Bug introduced by
The variable $step does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
124
        }
125
126
        return $ballot;
127
    }
128
129
    /**
130
     * Transfer the votes from one candidate to other candidates
131
     *
132
     * @param  float     $surplus   The number of surplus votes to transfer
133
     * @param  Candidate $candidate The candidate being elected to transfer
134
     *                              the votes from
135
     * @return
136
     */
137
    protected function transferSurplusVotes(float $surplus, Candidate $candidate)
138
    {
139
    	$totalVotes = $candiate->getVotes();
0 ignored issues
show
Bug introduced by
The variable $candiate does not exist. Did you mean $candidate?

This check looks for variables that are accessed but have not been defined. It raises an issue if it finds another variable that has a similar name.

The variable may have been renamed without also renaming all references.

Loading history...
140
    	$candidateId = $candidate->getId();
141
142
    	foreach ($this->ballots as $i => $ballot)
143
    	{
144
        	if ($ballot->getLastChoice()->getId() == $candidateId)
0 ignored issues
show
Bug introduced by
The method getId cannot be called on $ballot->getLastChoice() (of type null|integer).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
145
        	{
146
		        $this->allocateVotes($ballot, $surplus, $totalVotes);
147
        	}
148
    	}
149
150
        return;
151
    }
152
153
    /**
154
     * Transfer the votes from one eliminated candidate to other candidates
155
     *
156
     * @param  Candidate $candidate  Candidate being eliminated to transfer
157
     *                               the votes from
158
     * @return
159
     */
160
    protected function transferEliminatedVotes(Candidate $candidate)
161
    {
162
    	$candidateId = $candidate->getId();
163
164
    	foreach ($this->ballots as $i => $ballot)
165
        {
166
        	if ($ballot->getLastChoice()->getId() == $candidateId)
0 ignored issues
show
Bug introduced by
The method getId cannot be called on $ballot->getLastChoice() (of type null|integer).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
167
        	{
168
		        $this->allocateVotes($ballot);
169
        	}
170
        }
171
172
        return;
173
    }
174
175
    /**
176
     * Elect a candidate after they've passed the threshold
177
     *
178
     * @param  \Michaelc\Voting\STV\Candidate $candidate
179
     * @return null
180
     */
181
    protected function electCandidate(Candidate $candidate)
182
    {
183
        if ($candidate->getVotes() < $this->quota)
184
        {
185
            throw new Exception("We shouldn't be electing someone who hasn't met the quota");
186
        }
187
188
        $candidate->setState(Candidate::ELECTED);
189
        $this->electedCandidates++;
190
191
        if ($this->electedCandidates < $this->election->getWinnersCount())
192
        {
193
        	$surplus = $candidate->getVotes() - $this->quota;
194
        	$this->transferVotes($surplus, $candidate);
0 ignored issues
show
Bug introduced by
The method transferVotes() does not seem to exist on object<Michaelc\Voting\STV\VoteHandler>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
195
        }
196
197
        return;
198
    }
199
200
    /**
201
     * Eliminate the candidate(s) with the lowest number of votes
202
     * and reallocated their votes
203
     *
204
     * @param  array  $candidates 	Array of active candidates
205
     * @return int 					Number of candidates eliminated
206
     */
207
    protected function eliminateCandidates(array &$candidates): int
208
    {
209
    	$minimum = 0;
210
211
    	foreach ($candidates as $i => $candidate)
212
        {
213
            if ($candidate->getVotes() > $minimum)
214
            {
215
                $minimum = $candidate->getVotes();
216
                unset($minimumCandidates);
217
                $minimumCandidates[] = $candidate;
0 ignored issues
show
Coding Style Comprehensibility introduced by
$minimumCandidates was never initialized. Although not strictly required by PHP, it is generally a good practice to add $minimumCandidates = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
218
            }
219
            elseif ($candidate->getVotes() == $minimum)
220
            {
221
                $minimumCandidates[] = $candidate;
0 ignored issues
show
Bug introduced by
The variable $minimumCandidates does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
222
            }
223
        }
224
225
        foreach($minimumCandidates as $minimumCandidate)
0 ignored issues
show
Coding Style introduced by
Expected 1 space after FOREACH keyword; 0 found
Loading history...
226
        {
227
        	$this->transferEliminatedVotes($minimumCandidate);
228
        	$minimumCandidate->setState(Candidate::DEFEATED);
229
        }
230
231
        return count($minimumCandidates);
232
    }
233
234
    /**
235
     * Get the quota to win
236
     *
237
     * @return int
238
     */
239 1
    public function getQuota(): int
240
    {
241 1
        return floor(
242 1
            ($this->election->getNumBallots() /
243 1
                ($this->election->getWinnersCount() + 1)
244
            )
245 1
            + 1);
246
    }
247
}
248