Passed
Branch develop (f993a2)
by Julien
06:23
created

TreeGen   C

Complexity

Total Complexity 55

Size/Duplication

Total Lines 518
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 8

Importance

Changes 0
Metric Value
dl 0
loc 518
c 0
b 0
f 0
wmc 55
lcom 2
cbo 8
rs 6.8

36 Methods

Rating   Name   Duplication   Size   Complexity  
addFighterToGroup() 0 1 ?
chunkAndShuffle() 0 1 ?
createByeFighter() 0 1 ?
generateAllTrees() 0 1 ?
generateFights() 0 1 ?
generateGroupsForRound() 0 1 ?
getByeGroup() 0 1 ?
getFighter() 0 1 ?
getFighters() 0 1 ?
getNumRounds() 0 1 ?
syncGroup() 0 1 ?
A __construct() 0 7 1
A run() 0 6 1
B update() 0 37 6
A getMaxFightersByEntity() 0 9 1
A getFightersByEntity() 0 11 2
B getTreeSize() 0 22 4
A repart() 0 14 4
A insertByes() 0 13 3
A saveGroup() 0 18 3
A createByeGroup() 0 10 2
A adjustFightersGroupWithByes() 0 15 1
A getPreviousRound() 0 6 1
A getParentGroup() 0 7 1
A getFightersByArea() 0 12 1
A addParentToChildren() 0 19 3
A getFullFighterList() 0 16 3
A shouldInsertBye() 0 4 3
A destroyPreviousFights() 0 6 1
A generateNextRoundsFights() 0 9 2
A updateParentFight() 0 13 3
A chooseAndUpdateParentFight() 0 17 3
A getNumArea() 0 12 1
A generateAllFights() 0 8 1
A getWinnerId() 0 7 2
A updateFight() 0 15 2

How to fix   Complexity   

Complex Class

Complex classes like TreeGen 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 TreeGen, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Xoco70\LaravelTournaments\TreeGen;
4
5
use Illuminate\Database\QueryException;
6
use Illuminate\Support\Collection;
7
use Illuminate\Http\Request;
8
use Illuminate\Support\Facades\DB;
9
use Xoco70\LaravelTournaments\Contracts\TreeGenerable;
10
use Xoco70\LaravelTournaments\Exceptions\DuplicatedException;
11
use Xoco70\LaravelTournaments\Exceptions\DuplicatedFighterException;
12
use Xoco70\LaravelTournaments\Exceptions\TreeGenerationException;
13
use Xoco70\LaravelTournaments\Models\Championship;
14
use Xoco70\LaravelTournaments\Models\Fight;
15
use Xoco70\LaravelTournaments\Models\FightersGroup;
16
17
abstract class TreeGen implements TreeGenerable
18
{
19
    protected $groupBy;
20
    protected $tree;
21
    public $championship;
22
    public $settings;
23
    protected $numFighters;
24
25
    abstract protected function addFighterToGroup(FightersGroup $group, $fighter);
26
27
    abstract protected function chunkAndShuffle(Collection $fightersByEntity);
28
29
    abstract protected function createByeFighter();
30
31
    abstract protected function generateAllTrees();
32
33
    abstract protected function generateFights();
34
35
    abstract protected function generateGroupsForRound(Collection $fightersByArea, $round);
36
37
    abstract protected function getByeGroup($fighters);
38
39
    abstract protected function getFighter($fighterId);
40
41
    abstract protected function getFighters();
42
43
    abstract protected function getNumRounds($fightersCount);
44
45
    abstract protected function syncGroup(FightersGroup $group, $fighters);
46
47
    /**
48
     * @param Championship $championship
49
     * @param $groupBy
50
     */
51
    public function __construct(Championship $championship, $groupBy)
52
    {
53
        $this->championship = $championship;
54
        $this->groupBy = $groupBy;
55
        $this->settings = $championship->getSettings();
56
        $this->tree = new Collection();
57
    }
58
59
    /**
60
     * Generate tree groups for a championship.
61
     *
62
     * @throws TreeGenerationException
63
     */
64
    public function run()
65
    {
66
        $this->championship->fightersGroups()->delete();
67
        $this->generateAllTrees();
68
        $this->generateAllFights();
69
    }
70
71
    /**
72
     * Generate tree groups for a championship.
73
     *
74
     * @throws TreeGenerationException
75
     */
76
    public function update(Request $request)
77
    {
78
        $fighters = $request->fighters;
79
        $winners = $request->winners;
80
81
        // get fightersGroup with fights
82
        $query = FightersGroup::with('fights', 'competitors', 'teams')
0 ignored issues
show
Bug introduced by
The method where does only exist in Illuminate\Database\Eloquent\Builder, but not in Illuminate\Database\Eloquent\Model.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
83
            ->where('championship_id', $this->championship->id);
84
85
//        if ($this->championship->hasPreliminary()) {
0 ignored issues
show
Unused Code Comprehensibility introduced by
59% 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...
86
//            $query = $query->where('round', '=', 1);
87
//        }
88
        $groups = $query->get();
89
        $numFighter = 0;
90
        // Open Transaction, and change Fights and groups_fighters
91
        DB::beginTransaction();
92
        try {
93
            foreach ($groups as $group) {
94
                foreach ($group->fights as $fight) {
95
                    // Update Fight
96
                    $olds = [$fight->c1, $fight->c2];
97
                    $numFighter = $this->updateFight($fighters, $numFighter, $fight, $winners);
98
                    $this->updateGroupFighters($group, $fight, $olds);
0 ignored issues
show
Bug introduced by
The method updateGroupFighters() does not exist on Xoco70\LaravelTournaments\TreeGen\TreeGen. Did you maybe mean update()?

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...
99
                }
100
            }
101
        } catch (QueryException $e) {
102
            $errorCode = $e->errorInfo[1];
103
            if ($errorCode == 1062) {
104
                DB::rollBack();
105
                back()->withErrors(trans('laravel-tournaments::core.duplicated_fighter_in_group'));
106
            }
107
        } catch (DuplicatedFighterException $e) {
108
            DB::rollBack();
109
            back()->withErrors(trans('laravel-tournaments::core.duplicated_fighter_in_group'));
110
        }
111
        DB::commit();
112
    }
113
114
    /**
115
     * Get the biggest entity group.
116
     *
117
     * @param $userGroups
118
     *
119
     * @return int
120
     */
121
    private function getMaxFightersByEntity($userGroups): int
122
    {
123
        return $userGroups
124
            ->sortByDesc(function ($group) {
125
                return $group->count();
126
            })
127
            ->first()
128
            ->count();
129
    }
130
131
    /**
132
     * Get Competitor's list ordered by entities
133
     * Countries for Internation Tournament, State for a National Tournament, etc.
134
     *
135
     * @param $fighters
136
     *
137
     * @return Collection
138
     */
139
    private function getFightersByEntity($fighters): Collection
140
    {
141
        // Right now, we are treating users and teams as equals.
142
        // It doesn't matter right now, because we only need name attribute which is common to both models
143
144
        // $this->groupBy contains federation_id, association_id, club_id, etc.
145
        if (($this->groupBy) != null) {
146
            return $fighters->groupBy($this->groupBy); // Collection of Collection
147
        }
148
        return $fighters->chunk(1); // Collection of Collection
149
    }
150
151
    /**
152
     * Get the size the first round will have.
153
     *
154
     * @param $fighterCount
155
     * @param $groupSize
156
     *
157
     * @return int
158
     */
159
    protected function getTreeSize($fighterCount, $groupSize)
160
    {
161
        $squareMultiplied = collect([1, 2, 4, 8, 16, 32, 64])
162
            ->map(function ($item) use ($groupSize) {
163
                return $item * $groupSize;
164
            }); // [4, 8, 16, 32, 64,...]
0 ignored issues
show
Unused Code Comprehensibility introduced by
73% 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...
165
166
        foreach ($squareMultiplied as $limit) {
167
            if ($fighterCount <= $limit) {
168
                $treeSize = $limit;
169
                $numAreas = $this->championship->getSettings()->fightingAreas;
170
                $fighterCountPerArea = $treeSize / $numAreas;
171
                if ($fighterCountPerArea < $groupSize) {
172
                    $treeSize = $treeSize * $numAreas;
173
                }
174
175
                return $treeSize;
176
            }
177
        }
178
179
        return 64 * $groupSize;
180
    }
181
182
    /**
183
     * Repart BYE in the tree,.
184
     *
185
     * @param $fighterGroups
186
     * @param int $max
187
     *
188
     * @return Collection
189
     */
190
    private function repart($fighterGroups, $max)
191
    {
192
        $fighters = new Collection();
193
        for ($i = 0; $i < $max; $i++) {
194
            foreach ($fighterGroups as $fighterGroup) {
195
                $fighter = $fighterGroup->values()->get($i);
196
                if ($fighter != null) {
197
                    $fighters->push($fighter);
198
                }
199
            }
200
        }
201
202
        return $fighters;
203
    }
204
205
    /**
206
     * Insert byes in an homogen way.
207
     *
208
     * @param Collection $fighters
209
     * @param Collection $byeGroup
210
     *
211
     * @return Collection
212
     */
213
    private function insertByes(Collection $fighters, Collection $byeGroup)
214
    {
215
        $bye = count($byeGroup) > 0 ? $byeGroup[0] : [];
216
        $sizeFighters = count($fighters);
217
        $sizeGroupBy = count($byeGroup);
218
219
        $frequency = $sizeGroupBy != 0
220
            ? (int)floor($sizeFighters / $sizeGroupBy)
221
            : -1;
222
223
        // Create Copy of $competitors
224
        return $this->getFullFighterList($fighters, $frequency, $sizeGroupBy, $bye);
225
    }
226
227
228
    /**
229
     * @param $order
230
     * @param $round
231
     * @param $parent
232
     *
233
     * @return FightersGroup
234
     */
235
    protected function saveGroup($order, $round, $parent): FightersGroup
236
    {
237
        $group = new FightersGroup();
238
        $this->championship->isSingleEliminationType()
239
            ? $group->area = $this->getNumArea($round, $order)
240
            : $group->area = 1; // Area limited to 1 in playoff
241
242
243
        $group->order = $order;
244
        $group->round = $round;
245
        $group->championship_id = $this->championship->id;
246
        if ($parent != null) {
247
            $group->parent_id = $parent->id;
248
        }
249
        $group->save();
250
251
        return $group;
252
    }
253
254
    /**
255
     * @param int $groupSize
256
     *
257
     * @return Collection
258
     */
259
    public function createByeGroup($groupSize): Collection
260
    {
261
        $byeFighter = $this->createByeFighter();
262
        $group = new Collection();
263
        for ($i = 0; $i < $groupSize; $i++) {
264
            $group->push($byeFighter);
265
        }
266
267
        return $group;
268
    }
269
270
    /**
271
     * @param $fighters
272
     * @param Collection $fighterGroups
273
     *
274
     * @return Collection
275
     */
276
    public function adjustFightersGroupWithByes($fighters, $fighterGroups): Collection
277
    {
278
        $tmpFighterGroups = clone $fighterGroups;
279
        $byeGroup = $this->getByeGroup($fighters);
280
281
        // Get biggest competitor's group
282
        $max = $this->getMaxFightersByEntity($tmpFighterGroups);
283
284
        // We reacommodate them so that we can mix them up and they don't fight with another competitor of his entity.
285
286
        $fighters = $this->repart($fighterGroups, $max);
287
        $fighters = $this->insertByes($fighters, $byeGroup);
288
289
        return $fighters;
290
    }
291
292
    /**
293
     * Get All Groups on previous round.
294
     *
295
     * @param $currentRound
296
     *
297
     * @return Collection
298
     */
299
    private function getPreviousRound($currentRound)
300
    {
301
        $previousRound = $this->championship->groupsByRound($currentRound + 1)->get();
302
303
        return $previousRound;
304
    }
305
306
    /**
307
     * Get the next group on the right ( parent ), final round being the ancestor.
308
     *
309
     * @param $matchNumber
310
     * @param Collection $previousRound
311
     *
312
     * @return mixed
313
     */
314
    private function getParentGroup($matchNumber, $previousRound)
315
    {
316
        $parentIndex = intval(($matchNumber + 1) / 2);
317
        $parent = $previousRound->get($parentIndex - 1);
318
319
        return $parent;
320
    }
321
322
    /**
323
     * Group Fighters by area.
324
     *
325
     * @throws TreeGenerationException
326
     *
327
     * @return Collection
328
     */
329
    protected function getFightersByArea()
330
    {
331
        // If previous trees already exists, delete all
332
        $areas = $this->settings->fightingAreas;
333
        $fighters = $this->getFighters();
334
335
        // Get Competitor's / Team list ordered by entities ( Federation, Assoc, Club, etc...)
336
        $fighterByEntity = $this->getFightersByEntity($fighters); // Chunk(1)
337
        $fightersWithBye = $this->adjustFightersGroupWithByes($fighters, $fighterByEntity);
338
        // Chunk user by areas
339
        return $fightersWithBye->chunk(count($fightersWithBye) / $areas);
340
    }
341
342
    /**
343
     * Attach a parent to every child for nestedSet Navigation.
344
     *
345
     * @param $numFightersElim
346
     */
347
    protected function addParentToChildren($numFightersElim)
348
    {
349
        $numRounds = $this->getNumRounds($numFightersElim);
350
        $groupsDesc = $this->championship
351
            ->fightersGroups()
352
            ->where('round', '<', $numRounds)
353
            ->orderByDesc('id')->get();
354
355
        $groupsDescByRound = $groupsDesc->groupBy('round');
356
357
        foreach ($groupsDescByRound as $round => $groups) {
358
            $previousRound = $this->getPreviousRound($round);
359
            foreach ($groups->reverse()->values() as $matchNumber => $group) {
360
                $parent = $this->getParentGroup($matchNumber + 1, $previousRound);
361
                $group->parent_id = $parent->id;
362
                $group->save();
363
            }
364
        }
365
    }
366
367
    /**
368
     * @param Collection $fighters
369
     * @param $frequency
370
     * @param $sizeGroupBy
371
     * @param $bye
372
     *
373
     * @return Collection
374
     */
375
    private function getFullFighterList(Collection $fighters, $frequency, $sizeGroupBy, $bye): Collection
376
    {
377
        $newFighters = new Collection();
378
        $count = 0;
379
        $byeCount = 0;
380
        foreach ($fighters as $fighter) {
381
            if ($this->shouldInsertBye($frequency, $sizeGroupBy, $count, $byeCount)) {
382
                $newFighters->push($bye);
383
                $byeCount++;
384
            }
385
            $newFighters->push($fighter);
386
            $count++;
387
        }
388
389
        return $newFighters;
390
    }
391
392
    /**
393
     * @param $frequency
394
     * @param $sizeGroupBy
395
     * @param $count
396
     * @param $byeCount
397
     *
398
     * @return bool
399
     */
400
    private function shouldInsertBye($frequency, $sizeGroupBy, $count, $byeCount): bool
401
    {
402
        return $frequency != -1 && $count % $frequency == 0 && $byeCount < $sizeGroupBy;
403
    }
404
405
    /**
406
     * Destroy Previous Fights for demo.
407
     */
408
    protected function destroyPreviousFights()
409
    {
410
        // Delete previous fight for this championship
411
        $arrGroupsId = $this->championship->fightersGroups()->get()->pluck('id');
412
        Fight::destroy($arrGroupsId);
413
    }
414
415
    /**
416
     * Generate Fights for next rounds.
417
     */
418
    public function generateNextRoundsFights()
419
    {
420
        $fightersCount = $this->championship->competitors->count() + $this->championship->teams->count();
421
        $maxRounds = $this->getNumRounds($fightersCount);
422
        for ($numRound = 1; $numRound < $maxRounds; $numRound++) {
423
            $groupsByRound = $this->championship->fightersGroups()->where('round', $numRound)->with('parent', 'children')->get();
424
            $this->updateParentFight($groupsByRound); // should be groupsByRound
425
        }
426
    }
427
428
    /**
429
     * @param $groupsByRound
430
     */
431
    protected function updateParentFight($groupsByRound)
432
    {
433
        foreach ($groupsByRound as $keyGroup => $group) {
434
            $parentGroup = $group->parent;
435
            if ($parentGroup == null) {
436
                break;
437
            }
438
            $parentFight = $parentGroup->fights->get(0);
439
440
            // determine whether c1 or c2 must be updated
441
            $this->chooseAndUpdateParentFight($keyGroup, $group, $parentFight);
442
        }
443
    }
444
445
    /**
446
     * @param $group
447
     * @param $parentFight
448
     */
449
    protected function chooseAndUpdateParentFight($keyGroup, FightersGroup $group, Fight $parentFight)
450
    {
451
        // we need to know if the child has empty fighters, is this BYE or undetermined
452
        if ($group->hasDeterminedParent()) {
453
            $valueToUpdate = $group->getValueToUpdate(); // This should be OK
454
            if ($valueToUpdate != null) {
455
                $fighterToUpdate = $group->getParentFighterToUpdate($keyGroup);
456
                $parentFight->$fighterToUpdate = $valueToUpdate;
457
                $parentFight->save();
458
                // Add fighter to pivot table
459
                $parentGroup = $parentFight->group;
460
461
                $fighter = $this->getFighter($valueToUpdate);
462
                $this->addFighterToGroup($parentGroup, $fighter);
463
            }
464
        }
465
    }
466
467
    /**
468
     * Calculate the area of the group ( group is still not created ).
469
     *
470
     * @param $round
471
     * @param $order
472
     *
473
     * @return int
474
     */
475
    protected function getNumArea($round, $order)
476
    {
477
        $totalAreas = $this->settings->fightingAreas;
478
        $numFighters = $this->championship->fighters->count(); // 4
479
        $numGroups = $this->getTreeSize($numFighters, $this->championship->getGroupSize()) / $this->championship->getGroupSize(); // 1 -> 1
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% 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...
480
481
        $areaSize = $numGroups / ($totalAreas * pow(2, $round - 1));
482
483
        $numArea = intval(ceil($order / $areaSize)); // if round == 4, and second match 2/2 = 1 BAD
484
//        dump($numArea);
485
        return $numArea;
486
    }
487
488
489
    protected function generateAllFights()
490
    {
491
        $this->generateFights(); // Abstract
492
493
        //TODO In direct elimination without Prelim, short_id are not generating well
494
        $this->generateNextRoundsFights();
495
        Fight::generateFightsId($this->championship);
496
    }
497
498
    /**
499
     * @param $winners
500
     * @param $numFighter
501
     * @param $fighters
502
     * @return int|null
503
     */
504
    protected function getWinnerId($winners, $numFighter, $fighters)
505
    {
506
        if ($winners[$numFighter] != null) {
507
            return $fighters[$numFighter];
508
        }
509
        return null;
510
    }
511
512
    /**
513
     * @param $fighters
514
     * @param $numFighter
515
     * @param $fight
516
     * @param $winners
517
     * @return int
518
     */
519
    protected function updateFight($fighters, $numFighter, $fight, $winners)
520
    {
521
        $fight->c1 = $fighters[$numFighter];
522
        $fight->winner_id = $this->getWinnerId($winners, $numFighter, $fighters);
523
        $fight->save();
524
        $numFighter++;
525
526
        $fight->c2 = $fighters[$numFighter];
527
        if ($fight->winner_id == null) {
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing $fight->winner_id of type integer|null against null; this is ambiguous if the integer can be zero. Consider using a strict comparison === instead.
Loading history...
528
            $fight->winner_id = $this->getWinnerId($winners, $numFighter, $fighters);
529
        }
530
        $fight->save();
531
        $numFighter++;
532
        return $numFighter;
533
    }
534
}
535