diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index 77d798b..771878c 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -40,6 +40,10 @@ jobs: if: success() || matrix.allow_failure run: phpcs -wps --colors + - name: PHPStan + uses: php-actions/phpstan@v3 + if: success() || matrix.allow_failure + test: name: Unit tests with PHP ${{ matrix.php }} on ${{ matrix.os }} runs-on: ${{ matrix.os }} diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 0000000..14e1edc --- /dev/null +++ b/phpstan-baseline.neon @@ -0,0 +1,36 @@ +parameters: + ignoreErrors: + - + message: "#^Method ipl\\\\Scheduler\\\\Cron\\:\\:getNextDue\\(\\) should return DateTimeInterface but returns DateTimeInterface\\|null\\.$#" + count: 2 + path: src/Cron.php + + - + message: "#^Method ipl\\\\Scheduler\\\\Cron\\:\\:jsonSerialize\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: src/Cron.php + + - + message: "#^Method ipl\\\\Scheduler\\\\RRule\\:\\:getNextDue\\(\\) should return DateTimeInterface but returns DateTimeInterface\\|null\\.$#" + count: 1 + path: src/RRule.php + + - + message: "#^Method ipl\\\\Scheduler\\\\RRule\\:\\:jsonSerialize\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: src/RRule.php + + - + message: "#^Parameter \\#1 \\$timezone of class DateTimeZone constructor expects string, string\\|null given\\.$#" + count: 1 + path: src/RRule.php + + - + message: "#^Parameter \\#2 \\$before of class Recurr\\\\Transformer\\\\Constraint\\\\BetweenConstraint constructor expects DateTimeInterface, DateTimeInterface\\|null given\\.$#" + count: 1 + path: src/RRule.php + + - + message: "#^Parameter \\#1 \\$timer of static method React\\\\EventLoop\\\\Loop\\:\\:cancelTimer\\(\\) expects React\\\\EventLoop\\\\TimerInterface, React\\\\EventLoop\\\\TimerInterface\\|null given\\.$#" + count: 1 + path: src/Scheduler.php diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..5891e2f --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,32 @@ +includes: + - phpstan-baseline.neon + +parameters: + level: max + + checkFunctionNameCase: true + checkInternalClassCaseSensitivity: true + treatPhpDocTypesAsCertain: false + + paths: + - src + + scanDirectories: + - vendor + + ignoreErrors: + - + messages: + - '#Unsafe usage of new static\(\)#' + - '#. but return statement is missing#' + reportUnmatched: false + + - '#Call to an undefined method DateTimeInterface::#' + + - '#Call to an undefined method React\\Promise\\PromiseInterface::#' + + - '#Method ipl\\Scheduler\\.* should return \$this.* but returns static#' + + - '#Parameter \#1 \$rrule of class Recurr\\Rule constructor expects string\|null, array.*\|string given#' + + - '#Parameter \#1 \$callback of function call_user_func_array expects callable\(\): mixed, array{Recurr\\Rule, string} given#' diff --git a/src/Common/Promises.php b/src/Common/Promises.php index 503fdf2..b896627 100644 --- a/src/Common/Promises.php +++ b/src/Common/Promises.php @@ -10,7 +10,7 @@ trait Promises { - /** @var SplObjectStorage */ + /** @var SplObjectStorage> */ protected $promises; /** @@ -100,9 +100,7 @@ protected function detachPromises(UuidInterface $uuid): array return []; } - /** @var ArrayObject $promises */ $promises = $this->promises[$uuid]; - $this->promises->detach($uuid); return $promises->getArrayCopy(); diff --git a/src/Common/Timers.php b/src/Common/Timers.php index ee923ae..2d0641f 100644 --- a/src/Common/Timers.php +++ b/src/Common/Timers.php @@ -8,7 +8,7 @@ trait Timers { - /** @var SplObjectStorage */ + /** @var SplObjectStorage */ protected $timers; /** diff --git a/src/Cron.php b/src/Cron.php index 3f391a7..639957b 100644 --- a/src/Cron.php +++ b/src/Cron.php @@ -9,6 +9,8 @@ use InvalidArgumentException; use ipl\Scheduler\Contract\Frequency; +use function ipl\Stdlib\get_php_type; + class Cron implements Frequency { public const PART_MINUTE = 0; @@ -20,10 +22,10 @@ class Cron implements Frequency /** @var CronExpression */ protected $cron; - /** @var DateTimeInterface Start time of this frequency */ + /** @var ?DateTimeInterface Start time of this frequency */ protected $start; - /** @var DateTimeInterface End time of this frequency */ + /** @var ?DateTimeInterface End time of this frequency */ protected $end; /** @var string String representation of the cron expression */ @@ -163,6 +165,15 @@ public static function isValid(string $expression): bool public static function fromJson(string $json): Frequency { $data = json_decode($json, true); + if (! is_array($data)) { + throw new InvalidArgumentException( + sprintf( + '%s expects json decoded value to be an array, got %s instead', + __METHOD__, + get_php_type($data) + ) + ); + } $self = new static($data['expression']); if (isset($data['start'])) { diff --git a/src/OneOff.php b/src/OneOff.php index 15a1a64..ebe945d 100644 --- a/src/OneOff.php +++ b/src/OneOff.php @@ -5,8 +5,11 @@ use DateTime; use DateTimeInterface; use DateTimeZone; +use InvalidArgumentException; use ipl\Scheduler\Contract\Frequency; +use function ipl\Stdlib\get_php_type; + class OneOff implements Frequency { /** @var DateTimeInterface Start time of this frequency */ @@ -46,6 +49,15 @@ public function getEnd(): ?DateTimeInterface public static function fromJson(string $json): Frequency { $data = json_decode($json, true); + if (! is_string($data)) { + throw new InvalidArgumentException( + sprintf( + '%s expects json decoded value to be string, got %s instead', + __METHOD__, + get_php_type($data) + ) + ); + } return new static(new DateTime($data)); } diff --git a/src/RRule.php b/src/RRule.php index d71e754..bfad0e5 100644 --- a/src/RRule.php +++ b/src/RRule.php @@ -15,6 +15,7 @@ use Recurr\Transformer\ArrayTransformerConfig; use Recurr\Transformer\Constraint\AfterConstraint; use Recurr\Transformer\Constraint\BetweenConstraint; +use stdClass; use function ipl\Stdlib\get_php_type; @@ -62,7 +63,7 @@ class RRule implements Frequency /** * Construct a new rrule instance * - * @param string|array $rule + * @param string|array $rule * * @throws InvalidRRule */ @@ -124,11 +125,17 @@ public static function fromFrequency(string $frequency): self public static function fromJson(string $json): Frequency { + /** @var stdClass $data */ $data = json_decode($json); $self = new static($data->rrule); $self->frequency = $data->frequency; if (isset($data->start)) { - $self->startAt(DateTime::createFromFormat(static::SERIALIZED_DATETIME_FORMAT, $data->start)); + $start = DateTime::createFromFormat(static::SERIALIZED_DATETIME_FORMAT, $data->start); + if (! $start) { + throw new InvalidArgumentException(sprintf('Cannot deserialize start time: %s', $data->start)); + } + + $self->startAt($start); } return $self; @@ -240,7 +247,7 @@ public function getFrequency(): string * @param int $limit Limit the recurrences to be generated to the given value * @param bool $include Whether to include the passed time in the result set * - * @return Generator + * @return Generator */ public function getNextRecurrences( DateTimeInterface $dateTime, @@ -296,7 +303,7 @@ public function jsonSerialize(): array * Redirect all public method calls to the underlying rrule object * * @param string $methodName - * @param array $args + * @param array $args * * @return mixed * diff --git a/src/Scheduler.php b/src/Scheduler.php index 8146383..25ad3a1 100644 --- a/src/Scheduler.php +++ b/src/Scheduler.php @@ -124,7 +124,7 @@ class Scheduler */ public const ON_TASK_EXPIRED = 'task-expired'; - /** @var SplObjectStorage The scheduled tasks of this scheduler */ + /** @var SplObjectStorage The scheduled tasks of this scheduler */ protected $tasks; public function __construct() @@ -262,7 +262,7 @@ public function schedule(Task $task, Frequency $frequency): self return $this; } - public function isValidEvent($event) + public function isValidEvent(string $event): bool { $events = array_flip([ static::ON_TASK_CANCEL, @@ -285,9 +285,10 @@ protected function cancelTask(Task $task): void { Loop::cancelTimer($this->detachTimer($task->getUuid())); + /** @var ExtendedPromiseInterface[] $promises */ $promises = $this->detachPromises($task->getUuid()); if (! empty($promises)) { - /** @var ExtendedPromiseInterface[] $promises */ + /** @var Promise\CancellablePromiseInterface $promise */ foreach ($promises as $promise) { $promise->cancel(); }