GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.

Period   F
last analyzed

Complexity

Total Complexity 84

Size/Duplication

Total Lines 520
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 4

Importance

Changes 0
Metric Value
wmc 84
lcom 2
cbo 4
dl 0
loc 520
rs 2
c 0
b 0
f 0

38 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 25 4
A make() 0 22 3
A startIncluded() 0 4 1
A startExcluded() 0 4 1
A endIncluded() 0 4 1
A endExcluded() 0 4 1
A getStart() 0 4 1
A getIncludedStart() 0 4 1
A getEnd() 0 4 1
A getIncludedEnd() 0 4 1
A length() 0 6 1
A overlapsWith() 0 14 3
A touchesWith() 0 14 3
A startsBefore() 0 4 1
A startsBeforeOrAt() 0 4 1
A startsAfter() 0 4 1
A startsAfterOrAt() 0 4 1
A startsAt() 0 7 1
A endsBefore() 0 7 1
A endsBeforeOrAt() 0 7 1
A endsAfter() 0 7 1
A endsAfterOrAt() 0 7 1
A endsAt() 0 7 1
A contains() 0 12 3
A equals() 0 14 3
A gap() 0 26 4
A overlapSingle() 0 18 4
A overlap() 0 16 3
A overlapAll() 0 14 3
B diffSingle() 0 41 6
A diff() 0 24 5
A getPrecisionMask() 0 4 1
A getIterator() 0 8 1
B resolveDate() 0 28 6
A resolveFormat() 0 12 4
A roundDate() 0 15 6
A createDateInterval() 0 13 1
A ensurePrecisionMatches() 0 8 2

How to fix   Complexity   

Complex Class

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

1
<?php
2
3
namespace Spatie\Period;
4
5
use DateInterval;
6
use DatePeriod;
7
use DateTime;
8
use DateTimeImmutable;
9
use DateTimeInterface;
10
use IteratorAggregate;
11
use Spatie\Period\Exceptions\CannotComparePeriods;
12
use Spatie\Period\Exceptions\InvalidDate;
13
use Spatie\Period\Exceptions\InvalidPeriod;
14
15
class Period implements IteratorAggregate
16
{
17
    /** @var \DateTimeImmutable */
18
    protected $start;
19
20
    /** @var \DateTimeImmutable */
21
    protected $end;
22
23
    /** @var \DateInterval */
24
    protected $interval;
25
26
    /** @var \DateTimeImmutable */
27
    private $includedStart;
28
29
    /** @var \DateTimeImmutable */
30
    private $includedEnd;
31
32
    /** @var int */
33
    private $boundaryExclusionMask;
34
35
    /** @var int */
36
    private $precisionMask;
37
38
    public function __construct(
39
        DateTimeImmutable $start,
40
        DateTimeImmutable $end,
41
        ?int $precisionMask = null,
42
        ?int $boundaryExclusionMask = null
43
    ) {
44
        if ($start > $end) {
45
            throw InvalidPeriod::endBeforeStart($start, $end);
46
        }
47
48
        $this->boundaryExclusionMask = $boundaryExclusionMask ?? Boundaries::EXCLUDE_NONE;
49
        $this->precisionMask = $precisionMask ?? Precision::DAY;
50
51
        $this->start = $this->roundDate($start, $this->precisionMask);
52
        $this->end = $this->roundDate($end, $this->precisionMask);
53
        $this->interval = $this->createDateInterval($this->precisionMask);
54
55
        $this->includedStart = $this->startIncluded()
56
            ? $this->start
57
            : $this->start->add($this->interval);
58
59
        $this->includedEnd = $this->endIncluded()
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->endIncluded() ? $...d->sub($this->interval) can also be of type false. However, the property $includedEnd is declared as type object<DateTimeImmutable>. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
60
            ? $this->end
61
            : $this->end->sub($this->interval);
62
    }
63
64
    /**
65
     * @param string|DateTimeInterface $start
66
     * @param string|DateTimeInterface $end
67
     * @param int|null $precisionMask
68
     * @param int|null $boundaryExclusionMask
69
     * @param string|null $format
70
     *
71
     * @return static
72
     */
73
    public static function make(
74
        $start,
75
        $end,
76
        ?int $precisionMask = null,
77
        ?int $boundaryExclusionMask = null,
78
        ?string $format = null
79
    ): Period {
80
        if ($start === null) {
81
            throw InvalidDate::cannotBeNull('Start date');
82
        }
83
84
        if ($end === null) {
85
            throw InvalidDate::cannotBeNull('End date');
86
        }
87
88
        return new static(
89
            static::resolveDate($start, $format),
90
            static::resolveDate($end, $format),
91
            $precisionMask,
92
            $boundaryExclusionMask
93
        );
94
    }
95
96
    public function startIncluded(): bool
97
    {
98
        return ! $this->startExcluded();
99
    }
100
101
    public function startExcluded(): bool
102
    {
103
        return Boundaries::EXCLUDE_START & $this->boundaryExclusionMask;
104
    }
105
106
    public function endIncluded(): bool
107
    {
108
        return ! $this->endExcluded();
109
    }
110
111
    public function endExcluded(): bool
112
    {
113
        return Boundaries::EXCLUDE_END & $this->boundaryExclusionMask;
114
    }
115
116
    public function getStart(): DateTimeImmutable
117
    {
118
        return $this->start;
119
    }
120
121
    public function getIncludedStart(): DateTimeImmutable
122
    {
123
        return $this->includedStart;
124
    }
125
126
    public function getEnd(): DateTimeImmutable
127
    {
128
        return $this->end;
129
    }
130
131
    public function getIncludedEnd(): DateTimeImmutable
132
    {
133
        return $this->includedEnd;
134
    }
135
136
    public function length(): int
137
    {
138
        $length = $this->getIncludedStart()->diff($this->getIncludedEnd())->days + 1;
139
140
        return $length;
141
    }
142
143
    public function overlapsWith(Period $period): bool
144
    {
145
        $this->ensurePrecisionMatches($period);
146
147
        if ($this->getIncludedStart() > $period->getIncludedEnd()) {
148
            return false;
149
        }
150
151
        if ($period->getIncludedStart() > $this->getIncludedEnd()) {
152
            return false;
153
        }
154
155
        return true;
156
    }
157
158
    public function touchesWith(Period $period): bool
159
    {
160
        $this->ensurePrecisionMatches($period);
161
162
        if ($this->getIncludedEnd()->diff($period->getIncludedStart())->days <= 1) {
163
            return true;
164
        }
165
166
        if ($this->getIncludedStart()->diff($period->getIncludedEnd())->days <= 1) {
167
            return true;
168
        }
169
170
        return false;
171
    }
172
173
    public function startsBefore(DateTimeInterface $date): bool
174
    {
175
        return $this->getIncludedStart() < $date;
176
    }
177
178
    public function startsBeforeOrAt(DateTimeInterface $date): bool
179
    {
180
        return $this->getIncludedStart() <= $date;
181
    }
182
183
    public function startsAfter(DateTimeInterface $date): bool
184
    {
185
        return $this->getIncludedStart() > $date;
186
    }
187
188
    public function startsAfterOrAt(DateTimeInterface $date): bool
189
    {
190
        return $this->getIncludedStart() >= $date;
191
    }
192
193
    public function startsAt(DateTimeInterface $date): bool
194
    {
195
        return $this->getIncludedStart()->getTimestamp() === $this->roundDate(
196
            $date,
197
            $this->precisionMask
198
        )->getTimestamp();
199
    }
200
201
    public function endsBefore(DateTimeInterface $date): bool
202
    {
203
        return $this->getIncludedEnd() < $this->roundDate(
204
                $date,
205
                $this->precisionMask
206
            );
207
    }
208
209
    public function endsBeforeOrAt(DateTimeInterface $date): bool
210
    {
211
        return $this->getIncludedEnd() <= $this->roundDate(
212
                $date,
213
                $this->precisionMask
214
            );
215
    }
216
217
    public function endsAfter(DateTimeInterface $date): bool
218
    {
219
        return $this->getIncludedEnd() > $this->roundDate(
220
                $date,
221
                $this->precisionMask
222
            );
223
    }
224
225
    public function endsAfterOrAt(DateTimeInterface $date): bool
226
    {
227
        return $this->getIncludedEnd() >= $this->roundDate(
228
                $date,
229
                $this->precisionMask
230
            );
231
    }
232
233
    public function endsAt(DateTimeInterface $date): bool
234
    {
235
        return $this->getIncludedEnd()->getTimestamp() === $this->roundDate(
236
                $date,
237
                $this->precisionMask
238
            )->getTimestamp();
239
    }
240
241
    public function contains(DateTimeInterface $date): bool
242
    {
243
        if ($this->roundDate($date, $this->precisionMask) < $this->getIncludedStart()) {
244
            return false;
245
        }
246
247
        if ($this->roundDate($date, $this->precisionMask) > $this->getIncludedEnd()) {
248
            return false;
249
        }
250
251
        return true;
252
    }
253
254
    public function equals(Period $period): bool
255
    {
256
        $this->ensurePrecisionMatches($period);
257
258
        if ($period->getIncludedStart()->getTimestamp() !== $this->getIncludedStart()->getTimestamp()) {
259
            return false;
260
        }
261
262
        if ($period->getIncludedEnd()->getTimestamp() !== $this->getIncludedEnd()->getTimestamp()) {
263
            return false;
264
        }
265
266
        return true;
267
    }
268
269
    /**
270
     * @param \Spatie\Period\Period $period
271
     *
272
     * @return static|null
273
     * @throws \Exception
274
     */
275
    public function gap(Period $period): ?Period
276
    {
277
        $this->ensurePrecisionMatches($period);
278
279
        if ($this->overlapsWith($period)) {
280
            return null;
281
        }
282
283
        if ($this->touchesWith($period)) {
284
            return null;
285
        }
286
287
        if ($this->getIncludedStart() >= $period->getIncludedEnd()) {
288
            return static::make(
289
                $period->getIncludedEnd()->add($this->interval),
290
                $this->getIncludedStart()->sub($this->interval),
0 ignored issues
show
Security Bug introduced by
It seems like $this->getIncludedStart()->sub($this->interval) targeting DateTimeImmutable::sub() can also be of type false; however, Spatie\Period\Period::make() does only seem to accept string|object<DateTimeInterface>, did you maybe forget to handle an error condition?
Loading history...
291
                $this->getPrecisionMask()
292
            );
293
        }
294
295
        return static::make(
296
            $this->getIncludedEnd()->add($this->interval),
297
            $period->getIncludedStart()->sub($this->interval),
0 ignored issues
show
Security Bug introduced by
It seems like $period->getIncludedStart()->sub($this->interval) targeting DateTimeImmutable::sub() can also be of type false; however, Spatie\Period\Period::make() does only seem to accept string|object<DateTimeInterface>, did you maybe forget to handle an error condition?
Loading history...
298
            $this->getPrecisionMask()
299
        );
300
    }
301
302
    /**
303
     * @param \Spatie\Period\Period $period
304
     *
305
     * @return static|null
306
     */
307
    public function overlapSingle(Period $period): ?Period
308
    {
309
        $this->ensurePrecisionMatches($period);
310
311
        $start = $this->getIncludedStart() > $period->getIncludedStart()
312
            ? $this->getIncludedStart()
313
            : $period->getIncludedStart();
314
315
        $end = $this->getIncludedEnd() < $period->getIncludedEnd()
316
            ? $this->getIncludedEnd()
317
            : $period->getIncludedEnd();
318
319
        if ($start > $end) {
320
            return null;
321
        }
322
323
        return static::make($start, $end, $this->getPrecisionMask());
324
    }
325
326
    /**
327
     * @param \Spatie\Period\Period ...$periods
328
     *
329
     * @return \Spatie\Period\PeriodCollection|static[]
330
     */
331
    public function overlap(Period ...$periods): PeriodCollection
332
    {
333
        $overlapCollection = new PeriodCollection();
334
335
        foreach ($periods as $period) {
336
            $overlap = $this->overlapSingle($period);
337
338
            if ($overlap === null) {
339
                continue;
340
            }
341
342
            $overlapCollection[] = $overlap;
343
        }
344
345
        return $overlapCollection;
346
    }
347
348
    /**
349
     * @param \Spatie\Period\Period ...$periods
350
     *
351
     * @return static
352
     */
353
    public function overlapAll(Period ...$periods): Period
354
    {
355
        $overlap = clone $this;
356
357
        if (! count($periods)) {
358
            return $overlap;
359
        }
360
361
        foreach ($periods as $period) {
362
            $overlap = $overlap->overlapSingle($period);
363
        }
364
365
        return $overlap;
366
    }
367
368
    public function diffSingle(Period $period): PeriodCollection
369
    {
370
        $this->ensurePrecisionMatches($period);
371
372
        $periodCollection = new PeriodCollection();
373
374
        if (! $this->overlapsWith($period)) {
375
            $periodCollection[] = clone $this;
376
            $periodCollection[] = clone $period;
377
378
            return $periodCollection;
379
        }
380
381
        $overlap = $this->overlapSingle($period);
382
383
        $start = $this->getIncludedStart() < $period->getIncludedStart()
384
            ? $this->getIncludedStart()
385
            : $period->getIncludedStart();
386
387
        $end = $this->getIncludedEnd() > $period->getIncludedEnd()
388
            ? $this->getIncludedEnd()
389
            : $period->getIncludedEnd();
390
391
        if ($overlap->getIncludedStart() > $start) {
392
            $periodCollection[] = static::make(
393
                $start,
394
                $overlap->getIncludedStart()->sub($this->interval),
395
                $this->getPrecisionMask()
396
            );
397
        }
398
399
        if ($overlap->getIncludedEnd() < $end) {
400
            $periodCollection[] = static::make(
401
                $overlap->getIncludedEnd()->add($this->interval),
402
                $end,
403
                $this->getPrecisionMask()
404
            );
405
        }
406
407
        return $periodCollection;
408
    }
409
410
    /**
411
     * @param \Spatie\Period\Period ...$periods
412
     *
413
     * @return \Spatie\Period\PeriodCollection|static[]
414
     */
415
    public function diff(Period ...$periods): PeriodCollection
416
    {
417
        if (count($periods) === 1 && ! $this->overlapsWith($periods[0])) {
418
            $collection = new PeriodCollection();
419
420
            $gap = $this->gap($periods[0]);
421
422
            if ($gap !== null) {
423
                $collection[] = $gap;
424
            }
425
426
            return $collection;
427
        }
428
429
        $diffs = [];
430
431
        foreach ($periods as $period) {
432
            $diffs[] = $this->diffSingle($period);
433
        }
434
435
        $collection = (new PeriodCollection($this))->overlap(...$diffs);
436
437
        return $collection;
438
    }
439
440
    public function getPrecisionMask(): int
441
    {
442
        return $this->precisionMask;
443
    }
444
445
    public function getIterator()
446
    {
447
        return new DatePeriod(
448
            $this->getIncludedStart(),
449
            $this->interval,
450
            $this->getIncludedEnd()->add($this->interval)
451
        );
452
    }
453
454
    protected static function resolveDate($date, ?string $format): DateTimeImmutable
455
    {
456
        if ($date instanceof DateTimeImmutable) {
457
            return $date;
458
        }
459
460
        if ($date instanceof DateTime) {
461
            return DateTimeImmutable::createFromMutable($date);
462
        }
463
464
        $format = static::resolveFormat($date, $format);
465
466
        if (! is_string($date)) {
467
            throw InvalidDate::forFormat($date, $format);
468
        }
469
470
        $dateTime = DateTimeImmutable::createFromFormat($format, $date);
471
472
        if ($dateTime === false) {
473
            throw InvalidDate::forFormat($date, $format);
474
        }
475
476
        if (strpos($format, ' ') === false) {
477
            $dateTime = $dateTime->setTime(0, 0, 0);
478
        }
479
480
        return $dateTime;
481
    }
482
483
    protected static function resolveFormat($date, ?string $format): string
484
    {
485
        if ($format !== null) {
486
            return $format;
487
        }
488
489
        if (strpos($format, ' ') === false && strpos($date, ' ') !== false) {
490
            return 'Y-m-d H:i:s';
491
        }
492
493
        return 'Y-m-d';
494
    }
495
496
    protected function roundDate(DateTimeInterface $date, int $precision): DateTimeImmutable
497
    {
498
        [$year, $month, $day, $hour, $minute, $second] = explode(' ', $date->format('Y m d H i s'));
0 ignored issues
show
Bug introduced by
The variable $year 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...
Bug introduced by
The variable $month seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
Bug introduced by
The variable $day seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
Bug introduced by
The variable $hour seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
Bug introduced by
The variable $minute seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
Bug introduced by
The variable $second seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
499
500
        $month = (Precision::MONTH & $precision) === Precision::MONTH ? $month : '01';
0 ignored issues
show
Bug introduced by
The variable $month seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
501
        $day = (Precision::DAY & $precision) === Precision::DAY ? $day : '01';
0 ignored issues
show
Bug introduced by
The variable $day seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
502
        $hour = (Precision::HOUR & $precision) === Precision::HOUR ? $hour : '00';
0 ignored issues
show
Bug introduced by
The variable $hour seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
503
        $minute = (Precision::MINUTE & $precision) === Precision::MINUTE ? $minute : '00';
0 ignored issues
show
Bug introduced by
The variable $minute seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
504
        $second = (Precision::SECOND & $precision) === Precision::SECOND ? $second : '00';
0 ignored issues
show
Bug introduced by
The variable $second seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
505
506
        return DateTimeImmutable::createFromFormat(
507
            'Y m d H i s',
508
            implode(' ', [$year, $month, $day, $hour, $minute, $second])
509
        );
510
    }
511
512
    protected function createDateInterval(int $precision): DateInterval
513
    {
514
        $interval = [
515
            Precision::SECOND => 'PT1S',
516
            Precision::MINUTE => 'PT1M',
517
            Precision::HOUR => 'PT1H',
518
            Precision::DAY => 'P1D',
519
            Precision::MONTH => 'P1M',
520
            Precision::YEAR => 'P1Y',
521
        ][$precision];
522
523
        return new DateInterval($interval);
524
    }
525
526
    protected function ensurePrecisionMatches(Period $period): void
527
    {
528
        if ($this->precisionMask === $period->precisionMask) {
529
            return;
530
        }
531
532
        throw CannotComparePeriods::precisionDoesNotMatch();
533
    }
534
}
535