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