Optimise::saveEmployeesMeetings()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 12
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 12
ccs 11
cts 11
cp 1
rs 9.4285
cc 2
eloc 8
nc 2
nop 1
crap 2
1
<?php
2
/**
3
 * Created by PhpStorm.
4
 * User: claudio
5
 * Date: 12/12/15
6
 * Time: 15.41
7
 */
8
9
namespace plunner\Console\Commands\Optimise;
10
11
use Illuminate\Console\Scheduling\Schedule;
12
use plunner\company;
13
use plunner\Events\Optimise\ErrorEvent;
14
use plunner\Events\Optimise\OkEvent;
15
16
/**
17
 * Class Optimise
18
 * @author Claudio Cardinale <[email protected]>
19
 * @copyright 2015 Claudio Cardinale
20
 * @version 1.0.0
21
 * @package plunner\Console\Commands\Optimise
22
 */
23
class Optimise
24
{
25
26
    private $max_time_slots;
27
    private $time_slots;
28
29
    //TODO timezone
30
    /**
31
     * @var \DateTime
32
     */
33
    private $startTime;
34
    /**
35
     * @var \DateTime
36
     */
37
    private $endTime;
38
39
    /**
40
     * @var Company
41
     */
42
    private $company;
43
44
    /**
45
     * @var Schedule laravel schedule object needed to perform command in background
46
     */
47
    private $schedule;
48
49
    /**
50
     * @var \Illuminate\Contracts\Foundation\Application;
51
     */
52
    private $laravel;
53
54
    /**
55
     * @var Solver
56
     */
57
    private $solver = null;
58
59
    //TODO clone
60
    //TODO to_string
61
62
    /**
63
     * Optimise constructor.
64
     * @param company $company
65
     * @param Schedule $schedule
66
     * @param \Illuminate\Contracts\Foundation\Application $laravel
67
     */
68 12
    public function __construct(company $company, Schedule $schedule, \Illuminate\Contracts\Foundation\Application $laravel)
69
    {
70 12
        $this->company = $company;
0 ignored issues
show
Documentation Bug introduced by
It seems like $company of type object<plunner\Company> is incompatible with the declared type object<plunner\Console\Commands\Optimise\Company> of property $company.

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...
71 12
        $this->schedule = $schedule;
72 12
        $this->laravel = $laravel;
73 12
        $this->max_time_slots = config('app.timeslots.max');
74 12
        $this->time_slots = config('app.timeslots.number');
75
76
77 12
        $this->setStartTime((new \DateTime())->modify('next monday'));
78 12
    }
79
80
81
    /**
82
     * @param \DateTime $startTime
83
     */
84 12
    public function setStartTime(\DateTime $startTime)
85
    {
86 12
        $this->startTime = clone $startTime;
87 12
        $this->endTime = clone $this->startTime;
88 12
        $this->endTime->add(new \DateInterval('PT' . ($this->time_slots *
89 12
                config('app.timeslots.duration')) . 'S'));
90 12
    }
91
92
    /**
93
     * @return int
94
     */
95 2
    public function getMaxTimeSlots()
96
    {
97 2
        return $this->max_time_slots;
98
    }
99
100
    /**
101
     * @param int $max_time_slots
102
     */
103 3
    public function setMaxTimeSlots($max_time_slots)
104
    {
105 3
        $this->max_time_slots = $max_time_slots;
106 3
    }
107
108
    /**
109
     * @return int
110
     */
111 3
    public function getTimeSlots()
112
    {
113 3
        return $this->time_slots;
114
    }
115
116
    /**
117
     * @param int $time_slots
118
     */
119 3
    public function setTimeSlots($time_slots)
120
    {
121 3
        $this->time_slots = $time_slots;
122 3
    }
123
124
125
    /**
126
     * @return Company
127
     */
128
    public function getCompany()
129
    {
130
        return $this->company;
131
    }
132
133
    /**
134
     * @param Company $company
135
     */
136
    public function setCompany($company)
137
    {
138
        $this->company = $company;
139
    }
140
141
    /**
142
     * @return Solver
143
     */
144 5
    public function getSolver()
145 4
    {
146 3
        return $this->solver;
147
    }
148
149
150
    /**
151
     * @return Optimise
152
     * @throws OptimiseException
153
     */
154 12
    public function optimise()
155
    {
156
        try {
157 12
            $solver = new Solver($this->schedule, $this->laravel);
0 ignored issues
show
Compatibility introduced by
$this->laravel of type object<Illuminate\Contra...Foundation\Application> is not a sub-type of object<Illuminate\Foundation\Application>. It seems like you assume a concrete implementation of the interface Illuminate\Contracts\Foundation\Application 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...
158 12
            $solver = $this->setData($solver);
159 6
            $solver = $solver->solve();
160 6
            $this->solver = $solver;
161 10
        }catch(OptimiseException $e) {
162 10
            if(!$e->isEmpty())
163 4
                \Event::fire(new ErrorEvent($this->company, $e->getMessage()));
164 6
            throw $e;
165
        }catch (\Exception $e) {
166
            //TODO use the correct exceptions to avoid to share private data
167
            \Event::fire(new ErrorEvent($this->company, $e->getMessage()));
168
            throw new OptimiseException('Optimising error', 0, $e);
169
            //TODO catch specif exception
170
        }
171 6
        return $this;
172
    }
173
174
    /**
175
     * @param Solver $solver
176
     * @return Solver
177
     * @throws OptimiseException
178
     */
179 12
    private function setData(Solver $solver)
180
    {
181 12
        $solver = $this->setTimeSlotsSolver($solver);
182 12
        $solver = $this->setUsers($solver);
183 9
        $solver = $this->setAllMeetingsInfo($solver);
184 6
        $solver = $this->setUserAvailability($solver);
185 6
        $solver = $this->setUsersMeetings($solver);
186 6
        return $solver;
187
    }
188
189
    //TODO fix php doc with exceptions
190
191
    /**
192
     * @param Solver $solver
193
     * @return Solver
194
     * @throws OptimiseException
195
     */
196 12
    private function setTimeSlotsSolver(Solver $solver)
197
    {
198 12
        return $solver->setTimeSlots($this->time_slots)->setMaxTimeSlots($this->max_time_slots);
199
    }
200
201
    /**
202
     * @param Solver $solver
203
     * @return Solver
204
     * @throws OptimiseException
205
     */
206 12
    private function setUsers(Solver $solver)
207
    {
208
        //since we consider busy timeslots, we need to get all users
209 12
        $users = $this->company->employees->pluck('id')->toArray();
210 12
        if(count($users) == 0)
211 9
            throw ((new OptimiseException("No users for this company"))->withEmpty(true));
212 9
        return $solver->setUsers($users);
213
    }
214
215
    /**
216
     * @param Solver $solver
217
     * @return Solver
218
     * @throws OptimiseException
219
     */
220 9
    private function setAllMeetingsInfo(Solver $solver)
221
    {
222
        /**
223
         * @var $meetings \Illuminate\Support\Collection
224
         */
225 9
        $meetings = collect($this->company->getMeetingsTimeSlots($this->startTime, $this->endTime));
226 9
        if($meetings->count() == 0)
227 7
            throw ((new OptimiseException("No meetings for this week"))->withEmpty(true));
228
        $timeslots = $meetings->groupBy('id')->map(function ($item) { //convert timeslots
229 6
            return $this->durationConverter($this->timeSlotsConverter($item));
230 6
        });
231 6
        return $solver->setMeetings($timeslots->keys()->toArray())
232 6
            ->setMeetingsDuration($meetings->pluck('duration', 'id')->toArray())
233 6
            ->setMeetingsAvailability(self::getAvailabilityArray($timeslots, $this->time_slots, $solver->getMeetings()));
234
    }
235
236
237
    /**
238
     * @param mixed $item
239
     * @return mixed
240
     */
241 6
    private function durationConverter($item)
242
    {
243
        return $item->each(function ($item2) {
244 6
            $item2->duration = $this->convertDuration((int)$item2->duration);
245 6
            return $item2;
246
            //TODO try catch
247 6
        });
248
    }
249
250
    /**
251
     * @param int $duration
252
     * @return int
253
     */
254 6
    static private function convertDuration($duration)
255
    {
256 6
        return (int)ceil($duration / config('app.timeslots.duration'));
257
    }
258
259
    /**
260
     * @param mixed $item
261
     * @return mixed
262
     */
263 6
    private function timeSlotsConverter($item)
264
    {
265
        return $item->each(function ($item2) {
266 6
            $item2->time_start = $this->toTimeSlot($item2->time_start);
267 6
            $item2->time_end = $this->toTimeSlot($item2->time_end);
268 6
            return $item2;
269
            //TODO try catch
270 6
        });
271
    }
272
273
    /**
274
     * @param mixed $time
275
     * @return int
276
     * @throws OptimiseException
277
     */
278 6
    private function toTimeSlot($time)
279
    {
280 6
        $dateTime = new \DateTime($time);
281 6
        $diff = $dateTime->diff($this->startTime);
282 6
        $diff = explode(':', $diff->format('%R:%d:%h:%i:%s'));
283 6
        $diff = $diff[1] * 86400 + $diff[2] * 3600 + $diff[3] * 60 + $diff[4];
284
        //if($diff[0] != '-' && $diff != 0)
285
        //  throw new OptimiseException('timeslot time <= startTime');
286
        //TODO fix check
287
        //TODO check if diff makes sense
288
        //TODO check upper limit
289 6
        return (int)(round($diff / config('app.timeslots.duration')) + 1); //TODO can round cause overlaps?
290
    }
291
292
    /**
293
     * @param \Illuminate\Support\Collection $timeSlots
294
     * @param bool|true $free if true the array is filled with 1 for timeslots values else with 0 for timeslots values
295
     * @param array $ids array of ids that we consider, if they are not present inside timeSlots we fill the entire row
296
     *      with the default value
297
     * @param int $timeSlotsN number of timeslots
298
     * @return array
299
     */
300 6
    static private function getAvailabilityArray(\Illuminate\Support\Collection $timeSlots, $timeSlotsN, array $ids, $free = true)
301
    {
302 6
        $ret = [];
303 6
        foreach ($ids as $id) {
304 6
            if(isset($timeSlots[$id]))
305 6
                $ret = self::fillTimeSlots($ret, $id, $timeSlots[$id], $free ? '1' : '0');
306 6
            $ret = self::fillRow($ret, $id, $timeSlotsN, $free ? '0' : '1');
307 4
        }
308
309 6
        return $ret;
310
    }
311
312
    /**
313
     * @param array $array
314
     * @param int $id
315
     * @param \Illuminate\Support\Collection $timeSlots
316
     * @param string $fill
317
     * @return array
318
     */
319 6
    static private function fillTimeSlots(array $array, $id, \Illuminate\Support\Collection $timeSlots, $fill = '0')
320
    {
321 6
        foreach ($timeSlots as $timeSlot) {
322 6
            if (!isset($array[$id]))
323 6
                $array[$id] = [];
324 6
            $array[$id] = self::arrayPadInterval($array[$id], $timeSlot->time_start, $timeSlot->time_end, $fill);
325 4
        }
326 6
        return $array;
327
    }
328
329
    /**
330
     * @param array $array
331
     * @param int $from
332
     * @param int $to
333
     * @param string $pad
334
     * @return array
335
     */
336 6
    static private function arrayPadInterval(array $array, $from, $to, $pad = '0')
337
    {
338 6
        for ($i = $from; $i < $to; $i++)
339 6
            $array[$i] = $pad;
340 6
        return $array;
341
    }
342
343
    /**
344
     * @param array $array
345
     * @param int $id
346
     * @param string $fill
347
     * @return array
348
     */
349 6
    static private function fillRow(array $array, $id, $until, $fill = '0')
350
    {
351 6
        for ($i = 1; $i <= $until; $i++) {
352 6
            if (!isset($array[$id][$i]))
353 6
                $array[$id][$i] = $fill;
354 4
        }
355
356 6
        return $array;
357
    }
358
359
    /**
360
     * @param Solver $solver
361
     * @return Solver
362
     * @throws OptimiseException
363
     */
364 6
    private function setUserAvailability(Solver $solver)
365
    {
366
        /**
367
         * @var $users \Illuminate\Support\Collection
368
         */
369 6
        $users = collect($this->company->getEmployeesTimeSlots($this->startTime, $this->endTime));
370
        //if($users->count() == 0)
371
        //    throw ((new OptimiseException("No users for this company"))->withEmpty(true));
372
        $timeslots = $users->groupBy('id')->map(function ($item) { //convert timeslots
373 6
            return $this->timeSlotsConverter($item);
374 6
        });
375 6
        return $solver->setUsersAvailability(self::getAvailabilityArray($timeslots, $this->time_slots, $solver->getUsers(),
376 6
            false));
377
    }
378
379
    /**
380
     * @param Solver $solver
381
     * @return Solver
382
     * @throws OptimiseException
383
     */
384 6
    private function setUsersMeetings(Solver $solver)
385
    {
386 6
        $users = $solver->getUsers();
387 6
        $meetings = $solver->getMeetings();
388
        /**
389
         * @var $usersMeetings \Illuminate\Support\Collection
390
         */
391 6
        $usersMeetings = collect($this->company->getUsersMeetings($users, $meetings))->groupBy('employee_id');
392 6
        if($usersMeetings->count() == 0)
393 4
            throw ((new OptimiseException("No users for any meeting"))->withEmpty(true));
394
395 6
        return $solver->setUsersMeetings(self::getUsersMeetingsArray($users, $meetings, $usersMeetings));
396
    }
397
398
    /**
399
     * @param array $users
400
     * @param array $meetings
401
     * @param \Illuminate\Support\Collection $usersMeetings
402
     * @return array
403
     */
404 6
    static private function getUsersMeetingsArray($users, $meetings, \Illuminate\Support\Collection $usersMeetings)
405
    {
406 6
        $ret = [];
407 6
        foreach ($users as $user) {
408 6
            $usersMeetingsTmp = $usersMeetings->get($user);
409 6
            foreach ($meetings as $meeting) {
410 6
                if ($usersMeetingsTmp != null && $usersMeetingsTmp->contains('meeting_id', $meeting)) {
411 6
                    $ret[$user][$meeting] = 1;
412 4
                } else {
413 6
                    $ret[$user][$meeting] = 0;
414
                }
415 4
            }
416 4
        }
417
418 6
        return $ret;
419
    }
420
421
    /**
422
     * @return Optimise
423
     * @throws OptimiseException
424
     */
425 6
    public function save()
426
    {
427 6
        if (!($this->solver instanceof Solver)) {
428
            \Event::fire(new ErrorEvent($this->company, 'solver is not an instace of Solver'));
429
            throw new OptimiseException('solver is not an instance of Solver');
430
            return;
0 ignored issues
show
Unused Code introduced by
return; does not seem to be reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
431
        }
432
        //TODO check results before save them
433
434
        try {
435 6
            $this->saveMeetings($this->solver);
436 6
            $this->saveEmployeesMeetings($this->solver);
437
            //TODO use the correct exceptions to avoid to share private data
438 4
        } catch (\Exception $e) {
439
            //TODO if OptimiseException throw itself
440
            \Event::fire(new ErrorEvent($this->company, $e->getMessage()));
441
            throw new OptimiseException('Optimising error', 0, $e);
442
            //TODO catch specif exception
443
        }
444
        //TODO Is this the correct place?
445 6
        \Event::fire(new OkEvent($this->company));
446 6
        return $this;
447
    }
448
449
    /**
450
     * @param Solver $solver
451
     */
452 6
    private function saveMeetings(Solver $solver)
453
    {
454 6
        $meetings = $solver->getYResults();
455 6
        foreach ($meetings as $id => $meeting) {
456 6
            $meetingO = \plunner\Meeting::findOrFail($id);//TODO catch error
457 6
            $meetingO->start_time = $this->toDateTime(array_search('1', $meeting));
458 6
            $meetingO->save();
459 4
        }
460 6
    }
461
462
    /**
463
     * @param int $timeslot
464
     * @return \DateTime
465
     */
466 6
    private function toDateTime($timeslot)
467
    {
468 6
        $ret = clone $this->startTime;
469
        //TODO check, because the meetings cannot have this date available -> this to avoid errors if we don't have a date for a meeting
470 6
        if ($timeslot <= 1) //false == 0
471 6
            return $ret;
472
        return $ret->add(new \DateInterval('PT' . (($timeslot - 1) * config('app.timeslots.duration')) . 'S'));
473
    }
474
475
    /**
476
     * @param Solver $solver
477
     */
478 6
    private function saveEmployeesMeetings(Solver $solver)
479
    {
480 6
        $employeesMeetings = $solver->getXResults();
481 6
        foreach ($employeesMeetings as $eId => $employeeMeetings) {
482 6
            $employee = \plunner\Employee::findOrFail($eId);
483 6
            $employeeMeetings = collect($employeeMeetings);
484 6
            $employeeMeetings = $employeeMeetings->filter(function ($item) {
485 6
                return $item == 1;
486 6
            });
487 6
            $employee->meetings()->attach($employeeMeetings->keys()->toArray());
488 4
        }
489
    }
490
}