Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
86.96% covered (warning)
86.96%
40 / 46
57.14% covered (warning)
57.14%
8 / 14
CRAP
0.00% covered (danger)
0.00%
0 / 1
SequentialBackgroundProcess
86.96% covered (warning)
86.96%
40 / 46
57.14% covered (warning)
57.14%
8 / 14
20.89
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
 getPayload
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getErrors
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getId
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getVersion
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getStep
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getRetries
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getStartTime
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getCompletionTime
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getStatus
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getResult
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 provideAllowedMethods
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 cancel
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 runStep
95.24% covered (success)
95.24%
20 / 21
0.00% covered (danger)
0.00%
0 / 1
5
1<?php
2namespace Apie\Core\BackgroundProcess;
3
4use Apie\Core\ApieLib;
5use Apie\Core\Attributes\AlwaysDisabled;
6use Apie\Core\Attributes\Context;
7use Apie\Core\Attributes\FakeCount;
8use Apie\Core\Attributes\Internal;
9use Apie\Core\Attributes\Policy;
10use Apie\Core\Attributes\Requires;
11use Apie\Core\Attributes\RuntimeCheck;
12use Apie\Core\Attributes\StaticCheck;
13use Apie\Core\Context\ApieContext;
14use Apie\Core\ContextConstants;
15use Apie\Core\Dto\MessageAndTimestamp;
16use Apie\Core\Entities\EntityWithStatesInterface;
17use Apie\Core\Enums\ConsoleCommand;
18use Apie\Core\Identifiers\PascalCaseSlug;
19use Apie\Core\Identifiers\Ulid;
20use Apie\Core\Lists\ItemHashmap;
21use Apie\Core\Lists\ItemList;
22use Apie\Core\Lists\MessageAndTimestampList;
23use Apie\Core\Lists\StringList;
24use Apie\Core\ValueObjects\DatabaseText;
25use DateTimeInterface;
26use ReflectionClass;
27use Throwable;
28
29#[FakeCount(0)]
30#[StaticCheck(new Policy('staticCanViewAny', enabledOnMissingRule: true))]
31#[RuntimeCheck(
32    new Policy('canView', 'canViewAny')
33)]
34class SequentialBackgroundProcess implements EntityWithStatesInterface
35{
36    private int $version;
37    private int $step;
38    private int $retries = 0;
39    private DateTimeInterface $startTime;
40    private ?DateTimeInterface $completionTime = null;
41    private DatabaseText $className;
42    private BackgroundProcessStatus $status = BackgroundProcessStatus::Active;
43    private SequentialBackgroundProcessIdentifier $id;
44    private mixed $result = null;
45    private MessageAndTimestampList $errors;
46
47    #[StaticCheck(new AlwaysDisabled())]
48    public function __construct(
49        BackgroundProcessDeclaration $backgroundProcessDeclaration,
50        private ItemHashmap|ItemList $payload
51    ) {
52        $this->className = new DatabaseText(get_debug_type($backgroundProcessDeclaration));
53        $this->version = $backgroundProcessDeclaration->getCurrentVersion();
54        $this->step = 0;
55        $this->startTime = ApieLib::getPsrClock()->now();
56        $this->id = new SequentialBackgroundProcessIdentifier(
57            new PascalCaseSlug((new ReflectionClass($backgroundProcessDeclaration))->getShortName()),
58            Ulid::createRandom()
59        );
60        $this->errors = new MessageAndTimestampList();
61    }
62
63    #[StaticCheck(new Policy('staticReadPayload', enabledOnMissingRule: true))]
64    #[RuntimeCheck(new Policy('readPayload'))]
65    public function getPayload(): ItemHashmap|ItemList
66    {
67        return $this->payload;
68    }
69
70    #[StaticCheck(new Policy('staticReadErrors', enabledOnMissingRule: true))]
71    #[RuntimeCheck(new Policy('readErrors'))]
72    public function getErrors(): MessageAndTimestampList
73    {
74        return $this->errors;
75    }
76
77    public function getId(): SequentialBackgroundProcessIdentifier
78    {
79        return $this->id;
80    }
81
82    #[StaticCheck(new Policy('staticReadVersion', enabledOnMissingRule: true))]
83    #[RuntimeCheck(new Policy('readVersion'))]
84    public function getVersion(): int
85    {
86        return $this->version;
87    }
88
89    #[StaticCheck(new Policy('staticReadStep', enabledOnMissingRule: true))]
90    #[RuntimeCheck(new Policy('readStep'))]
91    public function getStep(): int
92    {
93        return $this->step;
94    }
95
96    #[StaticCheck(new Policy('staticReadRetries', enabledOnMissingRule: true))]
97    #[RuntimeCheck(new Policy('readRetries'))]
98    public function getRetries(): int
99    {
100        return $this->retries;
101    }
102
103    #[StaticCheck(new Policy('staticReadTime', enabledOnMissingRule: true))]
104    #[RuntimeCheck(new Policy('readTime'))]
105    public function getStartTime(): DateTimeInterface
106    {
107        return $this->startTime;
108    }
109
110    #[StaticCheck(new Policy('staticReadTime', enabledOnMissingRule: true))]
111    #[RuntimeCheck(new Policy('readTime'))]
112    public function getCompletionTime(): ?DateTimeInterface
113    {
114        return $this->completionTime;
115    }
116
117    #[StaticCheck(new Policy('staticReadStatus', enabledOnMissingRule: true))]
118    #[RuntimeCheck(new Policy('readStatus'))]
119    public function getStatus(): BackgroundProcessStatus
120    {
121        return $this->status;
122    }
123
124    #[StaticCheck(new Policy('staticReadResult', enabledOnMissingRule: true))]
125    #[RuntimeCheck(new Policy('readResult'))]
126    public function getResult(): mixed
127    {
128        return $this->result;
129    }
130
131    #[Internal]
132    public function provideAllowedMethods(): StringList
133    {
134        if ($this->status === BackgroundProcessStatus::Active) {
135            return new StringList(['cancel', 'runStep']);
136        }
137
138        return new StringList([]);
139    }
140
141    #[StaticCheck(new Policy('staticCancelBackgroundProcess', enabledOnMissingRule: true))]
142    #[RuntimeCheck(new Policy('cancelBackgroundProcess'))]
143    public function cancel(): void
144    {
145        if ($this->status !== BackgroundProcessStatus::Active) {
146            throw new \LogicException('Process ' . $this->id . ' can not be executed!');
147        }
148        $this->status = BackgroundProcessStatus::Canceled;
149    }
150
151    #[StaticCheck(new Policy('staticRunStep', enabledOnMissingRule: new Requires(ConsoleCommand::class)))]
152    #[RuntimeCheck(new Policy('runStep'))]
153    public function runStep(#[Context()] ApieContext $apieContext): void
154    {
155        if ($this->status !== BackgroundProcessStatus::Active) {
156            throw new \LogicException('Process ' . $this->id . ' can not be executed!');
157        }
158        $apieContext = $apieContext->withContext(ContextConstants::BACKGROUND_PROCESS, $this->result);
159        $maxRetries = 1;
160        try {
161            $className = $this->className->toNative();
162            $maxRetries = $className::getMaxRetries($this->version);
163            $steps = array_values($className::retrieveDeclaration($this->version));
164            if (isset($steps[$this->step])) {
165                $this->result = call_user_func($steps[$this->step], $apieContext, $this->payload);
166                $this->step++;
167                $this->retries = 0;
168            } else {
169                $this->completionTime = ApieLib::getPsrClock()->now();
170                $this->status = BackgroundProcessStatus::Finished;
171            }
172        } catch (Throwable $error) {
173            $this->errors[] = new MessageAndTimestamp(
174                $error->getMessage(),
175                ApieLib::getPsrClock()->now()
176            );
177            $this->retries++;
178            if ($this->retries >= $maxRetries) {
179                $this->status = BackgroundProcessStatus::TooManyErrors;
180            }
181        }
182    }
183}