Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
94.44% covered (success)
94.44%
68 / 72
40.00% covered (danger)
40.00%
2 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApieAuditLogForMigrationCommand
94.44% covered (success)
94.44%
68 / 72
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.43% covered (success)
96.43%
54 / 56
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\Common\Other\AuditOrigin;
7use Apie\Core\Attributes\Auditable;
8use Apie\Core\BoundedContext\BoundedContext;
9use Apie\Core\BoundedContext\BoundedContextHashmap;
10use Apie\Core\BoundedContext\BoundedContextId;
11use Apie\Core\ContextBuilders\ContextBuilderFactory;
12use Apie\Core\ContextConstants;
13use Apie\Core\Datalayers\ApieDatalayer;
14use Apie\Core\Datalayers\Search\QuerySearch;
15use Apie\Core\Entities\EntityInterface;
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                    $origin = AuditOrigin::createFromContext($subApieContext);
114                    foreach ($chunk as $item) {
115                        $output->write(sprintf('%40s', Utils::toString($item->getId())));
116                        $stop = false;
117                        /** @var EntityInterface $item */
118                        $createdResource = $this->apieDatalayer->persistExisting($item, $boundedContextId);
119                        $this->apieDatalayer->persistNew(
120                            new AuditLog(
121                                new IdFriendlyEntityReference(
122                                    $boundedContextId,
123                                    NonEmptyString::fromNative($resource->getShortName()),
124                                    NonEmptyString::fromNative($createdResource->getId())
125                                ),
126                                SerializedPhpObject::createFromPhpObject($createdResource),
127                                new AuditMigration(),
128                                $origin,
129                                null
130                            ),
131                            $boundedContextId
132                        );
133                        $output->writeln(' Done');
134                    }
135                } while (!$stop || $limit !== null);
136            }
137        }
138        return Command::SUCCESS;
139    }
140}