Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
72.36% covered (warning)
72.36%
89 / 123
75.00% covered (warning)
75.00%
6 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
Dataset
72.36% covered (warning)
72.36%
89 / 123
75.00% covered (warning)
75.00%
6 / 8
21.41
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getId
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getHashmap
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getSqliteFile
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 createRandom
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
1
 toDatalayer
92.11% covered (success)
92.11%
35 / 38
0.00% covered (danger)
0.00%
0 / 1
6.02
 buildOpenApi
100.00% covered (success)
100.00%
36 / 36
100.00% covered (success)
100.00%
1 / 1
1
 run
0.00% covered (danger)
0.00%
0 / 31
0.00% covered (danger)
0.00%
0 / 1
20
1<?php
2namespace Apie\Dataset;
3
4use Apie\Common\ActionDefinitionProvider;
5use Apie\Common\Actions\CreateObjectAction;
6use Apie\Common\Actions\GetItemAction;
7use Apie\Common\Actions\GetListAction;
8use Apie\Common\Actions\ModifyObjectAction;
9use Apie\Common\Actions\RemoveObjectAction;
10use Apie\Common\Actions\RunItemMethodAction;
11use Apie\Common\ApieFacade;
12use Apie\Common\Wrappers\GeneralServiceFactory;
13use Apie\Core\Attributes\ActionOptions;
14use Apie\Core\Attributes\Context;
15use Apie\Core\Attributes\FakeCount;
16use Apie\Core\Attributes\FakeMethod;
17use Apie\Core\Attributes\Internal;
18use Apie\Core\Attributes\Policy;
19use Apie\Core\Attributes\RuntimeCheck;
20use Apie\Core\Attributes\StaticCheck;
21use Apie\Core\Attributes\StoreOptions;
22use Apie\Core\BoundedContext\BoundedContext;
23use Apie\Core\BoundedContext\BoundedContextHashmap;
24use Apie\Core\BoundedContext\BoundedContextId;
25use Apie\Core\Context\ApieContext;
26use Apie\Core\ContextConstants;
27use Apie\Core\Entities\EntityInterface;
28use Apie\Core\Enums\RequestMethod;
29use Apie\Core\FileStorage\FileStorageFactory;
30use Apie\Core\FileStorage\SqliteFile;
31use Apie\Core\FileStorage\StoredFile;
32use Apie\Core\Indexing\Indexer;
33use Apie\Core\Lists\ReflectionClassList;
34use Apie\Core\Lists\ReflectionMethodList;
35use Apie\Core\ValueObjects\EntityReference;
36use Apie\Core\ValueObjects\NonEmptyString;
37use Apie\Dataset\DummyApp\Resources\DummyUser;
38use Apie\DoctrineEntityConverter\Factories\PersistenceLayerFactory;
39use Apie\DoctrineEntityConverter\OrmBuilder as DoctrineEntityConverterOrmBuilder;
40use Apie\DoctrineEntityDatalayer\DoctrineEntityDatalayer;
41use Apie\DoctrineEntityDatalayer\EntityReindexer;
42use Apie\DoctrineEntityDatalayer\Factories\DoctrineListFactory;
43use Apie\DoctrineEntityDatalayer\Factories\EntityQueryFilterFactory;
44use Apie\DoctrineEntityDatalayer\IndexStrategy\DirectIndexStrategy;
45use Apie\DoctrineEntityDatalayer\OrmBuilder;
46use Apie\RestApi\EventListeners\OpenApiOperationAddedEventSubscriber;
47use Apie\RestApi\EventListeners\OpenApiTagsNormalizerSubscriber;
48use Apie\RestApi\EventListeners\PruneUnusedComponentsSubscriber;
49use Apie\RestApi\OpenApi\OpenApiGenerator;
50use Apie\RestApi\RouteDefinitions\RestApiRouteDefinitionProvider;
51use Apie\SchemaGenerator\ComponentsBuilderFactory;
52use Apie\Serializer\DecoderHashmap;
53use Apie\Serializer\Serializer;
54use Apie\StorageMetadata\DomainToStorageConverter;
55use cebe\openapi\spec\OpenApi;
56use cebe\openapi\Writer;
57use Psr\Log\NullLogger;
58use Symfony\Component\DependencyInjection\ServiceLocator;
59use Symfony\Component\EventDispatcher\EventDispatcher;
60
61#[FakeMethod('createRandom')]
62#[FakeCount(1)]
63#[StaticCheck(new Policy('staticCanViewAny', enabledOnMissingRule: true))]
64#[RuntimeCheck(
65    new Policy('canView', 'canViewAny')
66)]
67final class Dataset implements EntityInterface
68{
69    private DatasetIdentifier $id;
70
71    #[StoreOptions(noStorage: true)]
72    private ?DoctrineEntityDatalayer $datalayer = null;
73
74    protected SqliteFile $sqliteFile;
75
76    #[StaticCheck(new Policy('staticCreate', enabledOnMissingRule: true))]
77    #[RuntimeCheck(new Policy('create'))]
78    public function __construct(
79        #[StoreOptions(alwaysMixedData: true)]
80        protected BoundedContextHashmap $hashmap
81    ) {
82        $this->id = DatasetIdentifier::createRandom();
83        $this->sqliteFile = SqliteFile::createRandom();
84    }
85
86    public function getId(): DatasetIdentifier
87    {
88        return $this->id;
89    }
90
91    #[StaticCheck(new Policy('staticHashmap', enabledOnMissingRule: true))]
92    #[RuntimeCheck(new Policy('hashmap'))]
93    public function getHashmap(): BoundedContextHashmap
94    {
95        return $this->hashmap;
96    }
97
98    #[StaticCheck(new Policy('staticSqliteFile', enabledOnMissingRule: true))]
99    #[RuntimeCheck(new Policy('sqliteFile'))]
100    public function getSqliteFile(): SqliteFile
101    {
102        return $this->sqliteFile;
103    }
104
105    #[Internal]
106    public static function createRandom(): static
107    {
108        $instance = new self(
109            new BoundedContextHashmap([
110                'test' => new BoundedContext(
111                    new BoundedContextId('test'),
112                    new ReflectionClassList([new \ReflectionClass(DummyUser::class)]),
113                    new ReflectionMethodList()
114                )
115            ])
116        );
117        $datalayer = $instance->toDatalayer();
118        $datalayer->all(new \ReflectionClass(DummyUser::class), new BoundedContextId('test'))
119            ->getTotalCount();
120
121        return $instance;
122    }
123
124    #[Internal]
125    public function toDatalayer(): DoctrineEntityDatalayer
126    {
127        if ($this->datalayer) {
128            return $this->datalayer;
129        }
130        $serverPath = $this->sqliteFile->getOrSetOnServerPath();
131
132        $tempFolder = sys_get_temp_dir() . DIRECTORY_SEPARATOR . $this->hashmap->getUniqueHash();
133        if (!@mkdir($tempFolder) && !is_dir($tempFolder)) {
134            throw new \LogicException('Can not create temp folder ' . $tempFolder);
135        }
136        $entityPath = $tempFolder . DIRECTORY_SEPARATOR . 'entities';
137        if (!@mkdir($entityPath) && !is_dir($entityPath)) {
138            throw new \LogicException('Can not create entity folder ' . $entityPath);
139        }
140        $ormBuilder = new DoctrineEntityConverterOrmBuilder(
141            new PersistenceLayerFactory(),
142            $this->hashmap,
143            true
144        );
145        $ormBuilder = new OrmBuilder(
146            $ormBuilder,
147            buildOnce: false,
148            runMigrations: true,
149            devMode: true,
150            cache: null,
151            path: $entityPath,
152            connectionConfig: [
153                'driver' => 'pdo_sqlite',
154                'path' => $serverPath
155            ]
156        );
157        $domainToStorageConverter = DomainToStorageConverter::create(FileStorageFactory::create());
158        $doctrineListFactory = new DoctrineListFactory(
159            $ormBuilder,
160            new EntityQueryFilterFactory(),
161            $domainToStorageConverter
162        );
163        return $this->datalayer = new DoctrineEntityDatalayer(
164            $ormBuilder,
165            $domainToStorageConverter,
166            new DirectIndexStrategy(new EntityReindexer($ormBuilder, Indexer::create())),
167            $doctrineListFactory
168        );
169    }
170
171    #[StaticCheck(new Policy('staticBuildOpenApi', enabledOnMissingRule: true))]
172    #[RuntimeCheck(new Policy('buildOpenApi'))]
173    public function buildOpenApi(
174        BoundedContextId $boundedContextId,
175        #[Context(ContextConstants::BOUNDED_CONTEXT_ID)] ?BoundedContextId $datasetBoundedContextId = null,
176        #[Context(ContextConstants::RESOURCE_NAME)] string $resourceName = 'Dataset',
177        #[Context(ContextConstants::RESOURCE_ID)] ?string $resourceId = null,
178        #[Context] ?DecoderHashmap $decoderHashmap = null
179    ): StoredFile {
180        $id = $resourceId ?? $this->getId()->toNative();
181        $baseUrl = $datasetBoundedContextId?->toNative() ?? '';
182        $baseUrl .= '/' . $resourceName . '/' . $id . '/' . $boundedContextId->toNative();
183        $baseSpec = new OpenApi([
184            'openapi' => '3.0.0',
185            'info' => [
186                'title' => $resourceName . ' ' . $id . ' - ' . $boundedContextId->toNative(),
187                'version' => '1.0.0',
188            ],
189            'paths' => [],
190        ]);
191        $dispatcher = new EventDispatcher();
192        $dispatcher->addSubscriber(new OpenApiTagsNormalizerSubscriber($this->hashmap));
193        $dispatcher->addSubscriber(new PruneUnusedComponentsSubscriber());
194        $dispatcher->addSubscriber(new OpenApiOperationAddedEventSubscriber($this->toDatalayer()));
195
196        $generator = new OpenApiGenerator(
197            GeneralServiceFactory::createContextBuilderFactory(
198                $this->hashmap,
199                $decoderHashmap,
200                []
201            ),
202            ComponentsBuilderFactory::createComponentsBuilderFactory(),
203            new RestApiRouteDefinitionProvider(
204                new ActionDefinitionProvider(new ServiceLocator([])),
205                new NullLogger()
206            ),
207            Serializer::create(),
208            $dispatcher,
209            $baseUrl,
210            $baseSpec,
211        );
212        return StoredFile::createFromString(
213            Writer::writeToJson($generator->create($this->hashmap[$boundedContextId->toNative()])),
214            'application/json',
215            'openapi-' . $resourceName . '-' . $id . '-' . $boundedContextId->toNative() . '.json'
216        );
217    }
218
219    #[ActionOptions(
220        pathRemainder: '{targetBounded}/{targetName}/{targetId}',
221        requestMethod: RequestMethod::GET,
222        placeholders: [
223            'targetAction' => GetItemAction::class,
224        ]
225    )]
226    #[ActionOptions(
227        pathRemainder: '{targetBounded}/{targetName}/{targetId}',
228        requestMethod: RequestMethod::PUT,
229        placeholders: [
230            'targetAction' => CreateObjectAction::class,
231        ]
232    )]
233    #[ActionOptions(
234        pathRemainder: '{targetBounded}/{targetName}/{targetId}',
235        requestMethod: RequestMethod::PATCH,
236        placeholders: [
237            'targetAction' => ModifyObjectAction::class,
238        ]
239    )]
240    #[ActionOptions(
241        pathRemainder: '{targetBounded}/{targetName}/{targetId}',
242        requestMethod: RequestMethod::DELETE,
243        placeholders: [
244            'targetAction' => RemoveObjectAction::class,
245        ]
246    )]
247    #[ActionOptions(
248        pathRemainder: '{targetBounded}/{targetName}',
249        requestMethod: RequestMethod::GET,
250        placeholders: [
251            'targetAction' => GetListAction::class,
252        ]
253    )]
254    #[ActionOptions(
255        pathRemainder: '{targetBounded}/{targetName}/{targetId}/{targetMethod}',
256        requestMethod: RequestMethod::POST,
257        placeholders: [
258            'targetAction' => RunItemMethodAction::class,
259        ]
260    )]
261    public function run(
262        #[Context('targetAction')] string $targetAction,
263        #[Context('targetBounded')] BoundedContextId $boundedContextId,
264        #[Context('targetName')] NonEmptyString $resourceName,
265        #[Context('targetId')] ?NonEmptyString $resourceId = null,
266        #[Context('targetMethod')] ?NonEmptyString $targetMethod = null,
267        #[Context(ContextConstants::RAW_CONTENTS)] ?array $rawContents = null,
268        #[Context] ?DecoderHashmap $decoderHashmap = null
269    ): mixed {
270        $dataLayer = $this->toDatalayer();
271        $baseContext = [
272            ContextConstants::APIE_ACTION => $targetAction,
273            ContextConstants::BOUNDED_CONTEXT_ID => $boundedContextId->toNative(),
274            ContextConstants::RESOURCE_NAME => $resourceName->toNative(),
275        ];
276        if ($resourceId) {
277            $baseContext[ContextConstants::RESOURCE_ID] = $resourceId->toNative();
278            $baseContext[EntityReference::class] = new EntityReference(
279                $boundedContextId,
280                $resourceName,
281                $resourceId
282            );
283        }
284        if ($targetMethod) {
285            $baseContext[ContextConstants::METHOD_NAME] = $targetMethod->toNative();
286        }
287        if ($decoderHashmap) {
288            $baseContext[DecoderHashmap::class] = $decoderHashmap;
289        }
290        
291        $context = (new ApieContext($baseContext))
292            ->registerInstance($dataLayer);
293
294        $facade = new ApieFacade(
295            new RestApiRouteDefinitionProvider(
296                new ActionDefinitionProvider(new ServiceLocator([])),
297                new NullLogger()
298            ),
299            $this->hashmap,
300            Serializer::create(),
301            $dataLayer,
302        );
303        return $facade->createAction($context)
304            ->__invoke($context, $rawContents ?? [])
305            ->getResultAsNativeData();
306    }
307}