Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
72.44% |
92 / 127 |
|
75.00% |
6 / 8 |
CRAP | |
0.00% |
0 / 1 |
| Dataset | |
72.44% |
92 / 127 |
|
75.00% |
6 / 8 |
24.78 | |
0.00% |
0 / 1 |
| __construct | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
| getId | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| getHashmap | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| getSqliteFile | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| createRandom | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
1 | |||
| toDatalayer | |
90.48% |
38 / 42 |
|
0.00% |
0 / 1 |
8.06 | |||
| buildOpenApi | |
100.00% |
36 / 36 |
|
100.00% |
1 / 1 |
1 | |||
| run | |
0.00% |
0 / 31 |
|
0.00% |
0 / 1 |
20 | |||
| 1 | <?php |
| 2 | namespace Apie\Dataset; |
| 3 | |
| 4 | use Apie\Common\ActionDefinitionProvider; |
| 5 | use Apie\Common\Actions\CreateObjectAction; |
| 6 | use Apie\Common\Actions\GetItemAction; |
| 7 | use Apie\Common\Actions\GetListAction; |
| 8 | use Apie\Common\Actions\ModifyObjectAction; |
| 9 | use Apie\Common\Actions\RemoveObjectAction; |
| 10 | use Apie\Common\Actions\RunItemMethodAction; |
| 11 | use Apie\Common\ApieFacade; |
| 12 | use Apie\Common\Wrappers\GeneralServiceFactory; |
| 13 | use Apie\Core\Attributes\ActionOptions; |
| 14 | use Apie\Core\Attributes\Context; |
| 15 | use Apie\Core\Attributes\FakeCount; |
| 16 | use Apie\Core\Attributes\FakeMethod; |
| 17 | use Apie\Core\Attributes\Internal; |
| 18 | use Apie\Core\Attributes\Policy; |
| 19 | use Apie\Core\Attributes\RuntimeCheck; |
| 20 | use Apie\Core\Attributes\StaticCheck; |
| 21 | use Apie\Core\Attributes\StoreOptions; |
| 22 | use Apie\Core\BoundedContext\BoundedContext; |
| 23 | use Apie\Core\BoundedContext\BoundedContextHashmap; |
| 24 | use Apie\Core\BoundedContext\BoundedContextId; |
| 25 | use Apie\Core\Context\ApieContext; |
| 26 | use Apie\Core\ContextConstants; |
| 27 | use Apie\Core\Entities\EntityInterface; |
| 28 | use Apie\Core\Enums\RequestMethod; |
| 29 | use Apie\Core\FileStorage\FileStorageFactory; |
| 30 | use Apie\Core\FileStorage\SqliteFile; |
| 31 | use Apie\Core\FileStorage\StoredFile; |
| 32 | use Apie\Core\Indexing\Indexer; |
| 33 | use Apie\Core\Lists\ReflectionClassList; |
| 34 | use Apie\Core\Lists\ReflectionMethodList; |
| 35 | use Apie\Core\ValueObjects\EntityReference; |
| 36 | use Apie\Core\ValueObjects\NonEmptyString; |
| 37 | use Apie\Dataset\DummyApp\Resources\DummyUser; |
| 38 | use Apie\DoctrineEntityConverter\Factories\PersistenceLayerFactory; |
| 39 | use Apie\DoctrineEntityConverter\OrmBuilder as DoctrineEntityConverterOrmBuilder; |
| 40 | use Apie\DoctrineEntityDatalayer\DoctrineEntityDatalayer; |
| 41 | use Apie\DoctrineEntityDatalayer\EntityReindexer; |
| 42 | use Apie\DoctrineEntityDatalayer\Factories\DoctrineListFactory; |
| 43 | use Apie\DoctrineEntityDatalayer\Factories\EntityQueryFilterFactory; |
| 44 | use Apie\DoctrineEntityDatalayer\IndexStrategy\DirectIndexStrategy; |
| 45 | use Apie\DoctrineEntityDatalayer\OrmBuilder; |
| 46 | use Apie\RestApi\EventListeners\OpenApiOperationAddedEventSubscriber; |
| 47 | use Apie\RestApi\EventListeners\OpenApiTagsNormalizerSubscriber; |
| 48 | use Apie\RestApi\EventListeners\PruneUnusedComponentsSubscriber; |
| 49 | use Apie\RestApi\OpenApi\OpenApiGenerator; |
| 50 | use Apie\RestApi\RouteDefinitions\RestApiRouteDefinitionProvider; |
| 51 | use Apie\SchemaGenerator\ComponentsBuilderFactory; |
| 52 | use Apie\Serializer\DecoderHashmap; |
| 53 | use Apie\Serializer\Serializer; |
| 54 | use Apie\StorageMetadata\DomainToStorageConverter; |
| 55 | use cebe\openapi\spec\OpenApi; |
| 56 | use cebe\openapi\Writer; |
| 57 | use Psr\Log\NullLogger; |
| 58 | use Symfony\Component\DependencyInjection\ServiceLocator; |
| 59 | use 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 | )] |
| 67 | final 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 | } |