Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
94.20% covered (success)
94.20%
65 / 69
40.00% covered (danger)
40.00%
2 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApieAuditLogForMigrationCommand
94.20% covered (success)
94.20%
65 / 69
40.00% covered (danger)
40.00%
2 / 5
15.04
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 configure
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 skipBoundedContext
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 skipResource
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
3.03
 execute
96.23% covered (success)
96.23%
51 / 53
0.00% covered (danger)
0.00%
0 / 1
8
1<?php
2namespace Apie\Common\Command;
3
4use Apie\Common\Other\Audit\AuditMigration;
5use Apie\Common\Other\AuditLog;
6use Apie\Core\Attributes\Auditable;
7use Apie\Core\BoundedContext\BoundedContext;
8use Apie\Core\BoundedContext\BoundedContextHashmap;
9use Apie\Core\BoundedContext\BoundedContextId;
10use Apie\Core\ContextBuilders\ContextBuilderFactory;
11use Apie\Core\ContextConstants;
12use Apie\Core\Datalayers\ApieDatalayer;
13use Apie\Core\Datalayers\Search\QuerySearch;
14use Apie\Core\Entities\EntityInterface;
15use Apie\Core\Entities\RequiresRecalculatingInterface;
16use Apie\Core\Enums\ConsoleCommand;
17use Apie\Core\Lists\StringHashmap;
18use Apie\Core\ValueObjects\IdFriendlyEntityReference;
19use Apie\Core\ValueObjects\NonEmptyString;
20use Apie\Core\ValueObjects\Utils;
21use Apie\Serializer\ValueObjects\SerializedPhpObject;
22use ReflectionClass;
23use Symfony\Component\Console\Command\Command;
24use Symfony\Component\Console\Input\InputInterface;
25use Symfony\Component\Console\Input\InputOption;
26use Symfony\Component\Console\Output\OutputInterface;
27
28class ApieAuditLogForMigrationCommand extends Command
29{
30    private const CHUNKSIZE = 2000;
31    public function __construct(
32        private readonly BoundedContextHashmap $boundedContextHashmap,
33        private readonly ApieDatalayer $apieDatalayer,
34        private readonly ContextBuilderFactory $contextBuilderFactory
35    ) {
36        parent::__construct('apie:audit-log-for-migration');
37    }
38    protected function configure(): void
39    {
40        $this->setDescription('This command will create audit log entries for all resources that are being updated by a database migration.');
41        $this->addOption('limit', 'l', InputOption::VALUE_OPTIONAL, 'limit number of resources to check');
42        $this->addOption('resource', 'r', InputOption::VALUE_IS_ARRAY | InputOption::VALUE_OPTIONAL, 'only check this resource');
43        $this->addOption('bounded-context', 'b', InputOption::VALUE_IS_ARRAY | InputOption::VALUE_OPTIONAL, 'only check this bounded context');
44    }
45
46    private function skipBoundedContext(InputInterface $input, string $contextId): bool
47    {
48        $boundedContexts = $input->getOption('bounded-context');
49        if (empty($boundedContexts)) {
50            return false;
51        }
52        return !in_array($contextId, $boundedContexts);
53    }
54
55    /**
56     * @param ReflectionClass<EntityInterface> $resource
57     */
58    private function skipResource(InputInterface $input, ReflectionClass $resource): bool
59    {
60        $resources = $input->getOption('resource');
61        $attributes = $resource->getAttributes(Auditable::class);
62        if (empty($attributes)) {
63            return true;
64        }
65        if (empty($resources)) {
66            return false;
67        }
68        return !in_array($resource->getShortName(), $resources);
69    }
70
71    protected function execute(InputInterface $input, OutputInterface $output): int
72    {
73        $limit = $input->getOption('limit');
74        if ($limit !== null) {
75            $limit = (int) $limit;
76        }
77        $apieContext = $this->contextBuilderFactory->createGeneralContext([
78            ConsoleCommand::class => ConsoleCommand::CONSOLE_COMMAND,
79            ConsoleCommand::CONSOLE_COMMAND->value => true,
80            Command::class => $this,
81            ContextConstants::DISABLE_CONTEXT_FILTER => true,
82        ]);
83        /** @var BoundedContext $boundedContext */
84        foreach ($this->boundedContextHashmap as $contextId => $boundedContext) {
85            if ($this->skipBoundedContext($input, $contextId)) {
86                continue;
87            }
88            $subApieContext = $apieContext->registerInstance($boundedContext)
89                ->withContext(ContextConstants::BOUNDED_CONTEXT_ID, $contextId)
90                ->registerInstance(new BoundedContextId($contextId));
91            /** @var ReflectionClass<EntityInterface> $resource */
92            foreach ($boundedContext->resources as $resource) {
93                if ($this->skipResource($input, $resource)) {
94                    continue;
95                }
96                $offset = 0;
97                $boundedContextId = new BoundedContextId($contextId);
98                $output->writeln($resource->getShortName() . ' (' . $boundedContextId . ')');
99                $list = $this->apieDatalayer->all($resource, $boundedContextId);
100                do {
101                    $chunk = $list->toPaginatedResult(
102                        new QuerySearch(
103                            $offset,
104                            $limit ?? self::CHUNKSIZE,
105                            textSearch: null,
106                            searches: null,
107                            orderBy: new StringHashmap(['timestamp' => 'ASC']),
108                            apieContext: $subApieContext
109                        )
110                    );
111                    $offset++;
112                    $stop = true;
113                    foreach ($chunk as $item) {
114                        $output->write(sprintf('%40s', Utils::toString($item->getId())));
115                        $stop = false;
116                        /** @var EntityInterface $item */
117                        $createdResource = $this->apieDatalayer->persistExisting($item, $boundedContextId);
118                        $this->apieDatalayer->persistNew(
119                            new AuditLog(
120                                new IdFriendlyEntityReference(
121                                    $boundedContextId,
122                                    NonEmptyString::fromNative($resource->getShortName()),
123                                    NonEmptyString::fromNative($createdResource->getId())),
124                                    SerializedPhpObject::createFromPhpObject($createdResource),
125                                    new AuditMigration(),
126                                    null
127                                ),
128                                $boundedContextId
129                            );
130                        $output->writeln(' Done');
131                    }
132                } while (!$stop || $limit !== null);
133            }
134        }
135        return Command::SUCCESS;
136    }
137}