Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
72.44% covered (warning)
72.44%
92 / 127
75.00% covered (warning)
75.00%
6 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
Dataset
72.44% covered (warning)
72.44%
92 / 127
75.00% covered (warning)
75.00%
6 / 8
24.78
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
90.48% covered (success)
90.48%
38 / 42
0.00% covered (danger)
0.00%
0 / 1
8.06
 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        $proxyPath = $tempFolder . DIRECTORY_SEPARATOR . 'proxies';
141        if (!@mkdir($proxyPath) && !is_dir($proxyPath)) {
142            throw new \LogicException('Can not create proxy folder ' . $proxyPath);
143        }
144        $ormBuilder = new DoctrineEntityConverterOrmBuilder(
145            new PersistenceLayerFactory(),
146            $this->hashmap,
147            true
148        );
149        $ormBuilder = new OrmBuilder(
150            $ormBuilder,
151            buildOnce: false,
152            runMigrations: true,
153            devMode: true,
154            proxyDir: $proxyPath,
155            cache: null,
156            path: $entityPath,
157            connectionConfig: [
158                'driver' => 'pdo_sqlite',
159                'path' => $serverPath
160            ]
161        );
162        $domainToStorageConverter = DomainToStorageConverter::create(FileStorageFactory::create());
163        $doctrineListFactory = new DoctrineListFactory(
164            $ormBuilder,
165            new EntityQueryFilterFactory(),
166            $domainToStorageConverter
167        );
168        return $this->datalayer = new DoctrineEntityDatalayer(
169            $ormBuilder,
170            $domainToStorageConverter,
171            new DirectIndexStrategy(new EntityReindexer($ormBuilder, Indexer::create())),
172            $doctrineListFactory
173        );
174    }
175
176    #[StaticCheck(new Policy('staticBuildOpenApi', enabledOnMissingRule: true))]
177    #[RuntimeCheck(new Policy('buildOpenApi'))]
178    public function buildOpenApi(
179        BoundedContextId $boundedContextId,
180        #[Context(ContextConstants::BOUNDED_CONTEXT_ID)] ?BoundedContextId $datasetBoundedContextId = null,
181        #[Context(ContextConstants::RESOURCE_NAME)] string $resourceName = 'Dataset',
182        #[Context(ContextConstants::RESOURCE_ID)] ?string $resourceId = null,
183        #[Context] ?DecoderHashmap $decoderHashmap = null
184    ): StoredFile {
185        $id = $resourceId ?? $this->getId()->toNative();
186        $baseUrl = $datasetBoundedContextId?->toNative() ?? '';
187        $baseUrl .= '/' . $resourceName . '/' . $id . '/' . $boundedContextId->toNative();
188        $baseSpec = new OpenApi([
189            'openapi' => '3.0.0',
190            'info' => [
191                'title' => $resourceName . ' ' . $id . ' - ' . $boundedContextId->toNative(),
192                'version' => '1.0.0',
193            ],
194            'paths' => [],
195        ]);
196        $dispatcher = new EventDispatcher();
197        $dispatcher->addSubscriber(new OpenApiTagsNormalizerSubscriber($this->hashmap));
198        $dispatcher->addSubscriber(new PruneUnusedComponentsSubscriber());
199        $dispatcher->addSubscriber(new OpenApiOperationAddedEventSubscriber($this->toDatalayer()));
200
201        $generator = new OpenApiGenerator(
202            GeneralServiceFactory::createContextBuilderFactory(
203                $this->hashmap,
204                $decoderHashmap,
205                []
206            ),
207            ComponentsBuilderFactory::createComponentsBuilderFactory(),
208            new RestApiRouteDefinitionProvider(
209                new ActionDefinitionProvider(new ServiceLocator([])),
210                new NullLogger()
211            ),
212            Serializer::create(),
213            $dispatcher,
214            $baseUrl,
215            $baseSpec,
216        );
217        return StoredFile::createFromString(
218            Writer::writeToJson($generator->create($this->hashmap[$boundedContextId->toNative()])),
219            'application/json',
220            'openapi-' . $resourceName . '-' . $id . '-' . $boundedContextId->toNative() . '.json'
221        );
222    }
223
224    #[ActionOptions(
225        pathRemainder: '{targetBounded}/{targetName}/{targetId}',
226        requestMethod: RequestMethod::GET,
227        placeholders: [
228            'targetAction' => GetItemAction::class,
229        ]
230    )]
231    #[ActionOptions(
232        pathRemainder: '{targetBounded}/{targetName}/{targetId}',
233        requestMethod: RequestMethod::PUT,
234        placeholders: [
235            'targetAction' => CreateObjectAction::class,
236        ]
237    )]
238    #[ActionOptions(
239        pathRemainder: '{targetBounded}/{targetName}/{targetId}',
240        requestMethod: RequestMethod::PATCH,
241        placeholders: [
242            'targetAction' => ModifyObjectAction::class,
243        ]
244    )]
245    #[ActionOptions(
246        pathRemainder: '{targetBounded}/{targetName}/{targetId}',
247        requestMethod: RequestMethod::DELETE,
248        placeholders: [
249            'targetAction' => RemoveObjectAction::class,
250        ]
251    )]
252    #[ActionOptions(
253        pathRemainder: '{targetBounded}/{targetName}',
254        requestMethod: RequestMethod::GET,
255        placeholders: [
256            'targetAction' => GetListAction::class,
257        ]
258    )]
259    #[ActionOptions(
260        pathRemainder: '{targetBounded}/{targetName}/{targetId}/{targetMethod}',
261        requestMethod: RequestMethod::POST,
262        placeholders: [
263            'targetAction' => RunItemMethodAction::class,
264        ]
265    )]
266    public function run(
267        #[Context('targetAction')] string $targetAction,
268        #[Context('targetBounded')] BoundedContextId $boundedContextId,
269        #[Context('targetName')] NonEmptyString $resourceName,
270        #[Context('targetId')] ?NonEmptyString $resourceId = null,
271        #[Context('targetMethod')] ?NonEmptyString $targetMethod = null,
272        #[Context(ContextConstants::RAW_CONTENTS)] ?array $rawContents = null,
273        #[Context] ?DecoderHashmap $decoderHashmap = null
274    ): mixed {
275        $dataLayer = $this->toDatalayer();
276        $baseContext = [
277            ContextConstants::APIE_ACTION => $targetAction,
278            ContextConstants::BOUNDED_CONTEXT_ID => $boundedContextId->toNative(),
279            ContextConstants::RESOURCE_NAME => $resourceName->toNative(),
280        ];
281        if ($resourceId) {
282            $baseContext[ContextConstants::RESOURCE_ID] = $resourceId->toNative();
283            $baseContext[EntityReference::class] = new EntityReference(
284                $boundedContextId,
285                $resourceName,
286                $resourceId
287            );
288        }
289        if ($targetMethod) {
290            $baseContext[ContextConstants::METHOD_NAME] = $targetMethod->toNative();
291        }
292        if ($decoderHashmap) {
293            $baseContext[DecoderHashmap::class] = $decoderHashmap;
294        }
295        
296        $context = (new ApieContext($baseContext))
297            ->registerInstance($dataLayer);
298
299        $facade = new ApieFacade(
300            new RestApiRouteDefinitionProvider(
301                new ActionDefinitionProvider(new ServiceLocator([])),
302                new NullLogger()
303            ),
304            $this->hashmap,
305            Serializer::create(),
306            $dataLayer,
307        );
308        return $facade->createAction($context)
309            ->__invoke($context, $rawContents ?? [])
310            ->getResultAsNativeData();
311    }
312}