Algo-Web /
POData-Laravel
| 1 | <?php |
||
| 2 | |||
| 3 | declare(strict_types=1); |
||
| 4 | |||
| 5 | namespace AlgoWeb\PODataLaravel\Providers; |
||
| 6 | |||
| 7 | use AlgoWeb\PODataLaravel\Models\ClassReflectionHelper; |
||
| 8 | use AlgoWeb\PODataLaravel\Models\IMetadataRelationshipContainer; |
||
| 9 | use AlgoWeb\PODataLaravel\Models\MetadataRelationshipContainer; |
||
| 10 | use AlgoWeb\PODataLaravel\Models\MetadataTrait; |
||
| 11 | use AlgoWeb\PODataLaravel\Models\ObjectMap\Entities\Associations\AssociationMonomorphic; |
||
| 12 | use AlgoWeb\PODataLaravel\Models\ObjectMap\Entities\Associations\AssociationStubRelationType; |
||
| 13 | use AlgoWeb\PODataLaravel\Models\ObjectMap\Entities\Associations\AssociationType; |
||
| 14 | use AlgoWeb\PODataLaravel\Models\ObjectMap\Entities\EntityField; |
||
| 15 | use AlgoWeb\PODataLaravel\Models\ObjectMap\Entities\EntityFieldType; |
||
| 16 | use AlgoWeb\PODataLaravel\Models\ObjectMap\Entities\EntityGubbins; |
||
| 17 | use AlgoWeb\PODataLaravel\Models\ObjectMap\Map; |
||
| 18 | use Cruxinator\ClassFinder\ClassFinder; |
||
| 19 | use Illuminate\Contracts\Container\BindingResolutionException; |
||
| 20 | 1 | use Illuminate\Database\Eloquent\Model; |
|
| 21 | use Illuminate\Support\Facades\App; |
||
| 22 | 1 | use Illuminate\Support\Facades\Cache; |
|
| 23 | use Illuminate\Support\Facades\Schema as Schema; |
||
| 24 | use Illuminate\Support\Str; |
||
| 25 | 1 | use POData\Common\InvalidOperationException; |
|
| 26 | use POData\Providers\Metadata\ResourceEntityType; |
||
| 27 | use POData\Providers\Metadata\ResourceStreamInfo; |
||
| 28 | 1 | use POData\Providers\Metadata\SimpleMetadataProvider; |
|
| 29 | 1 | use POData\Providers\Metadata\Type\TypeCode; |
|
| 30 | |||
| 31 | class MetadataProvider extends MetadataBaseProvider |
||
| 32 | { |
||
| 33 | use MetadataProviderStepTrait; |
||
| 34 | |||
| 35 | /** @var array<array> */ |
||
| 36 | protected $multConstraints = ['0..1' => ['1'], '1' => ['0..1', '*'], '*' => ['1', '*']]; |
||
| 37 | /** @var string */ |
||
| 38 | protected static $metaNAMESPACE = 'Data'; |
||
| 39 | /** @var bool */ |
||
| 40 | protected static $isBooted = false; |
||
| 41 | const POLYMORPHIC = 'polyMorphicPlaceholder'; |
||
| 42 | const POLYMORPHIC_PLURAL = 'polyMorphicPlaceholders'; |
||
| 43 | |||
| 44 | /** |
||
| 45 | * @var Map The completed object map set at post Implement; |
||
| 46 | */ |
||
| 47 | private $completedObjectMap; |
||
| 48 | |||
| 49 | /** |
||
| 50 | * @return \AlgoWeb\PODataLaravel\Models\ObjectMap\Map |
||
| 51 | */ |
||
| 52 | public function getObjectMap() |
||
| 53 | { |
||
| 54 | return $this->completedObjectMap; |
||
| 55 | } |
||
| 56 | |||
| 57 | /** @var IMetadataRelationshipContainer|null */ |
||
| 58 | protected $relationHolder; |
||
| 59 | |||
| 60 | public function __construct($app) |
||
| 61 | { |
||
| 62 | parent::__construct($app); |
||
| 63 | self::$isBooted = false; |
||
| 64 | } |
||
| 65 | |||
| 66 | /** |
||
| 67 | * @param string[] $modelNames |
||
| 68 | * @throws InvalidOperationException |
||
| 69 | * @throws \Doctrine\DBAL\DBALException |
||
| 70 | * @throws \ReflectionException |
||
| 71 | * @return Map |
||
| 72 | */ |
||
| 73 | private function extract(array $modelNames): Map |
||
| 74 | { |
||
| 75 | /** @var Map $objectMap */ |
||
| 76 | $objectMap = App::make('objectmap'); |
||
| 77 | foreach ($modelNames as $modelName) { |
||
| 78 | try { |
||
| 79 | /** @var MetadataTrait $modelInstance */ |
||
| 80 | $modelInstance = App::make($modelName); |
||
| 81 | } catch (BindingResolutionException $e) { |
||
| 82 | // if we can't instantiate modelName for whatever reason, move on |
||
| 83 | continue; |
||
| 84 | } |
||
| 85 | $gubbins = $modelInstance->extractGubbins(); |
||
| 86 | $isEmpty = 0 === count($gubbins->getFields()); |
||
| 87 | $inArtisan = $this->isRunningInArtisan(); |
||
| 88 | if (!($isEmpty && $inArtisan)) { |
||
| 89 | $objectMap->addEntity($gubbins); |
||
| 90 | } |
||
| 91 | } |
||
| 92 | $this->handleCustomFunction($objectMap, self::$afterExtract); |
||
| 93 | return $objectMap; |
||
| 94 | } |
||
| 95 | |||
| 96 | /** |
||
| 97 | * @param Map $objectMap |
||
| 98 | * @throws InvalidOperationException |
||
| 99 | * @return Map |
||
| 100 | */ |
||
| 101 | private function unify(Map $objectMap) |
||
| 102 | { |
||
| 103 | /** @var IMetadataRelationshipContainer $mgh */ |
||
| 104 | $mgh = $this->getRelationHolder(); |
||
| 105 | foreach ($objectMap->getEntities() as $entity) { |
||
| 106 | $mgh->addEntity($entity); |
||
| 107 | } |
||
| 108 | $objectMap->setAssociations($mgh->getRelations()); |
||
| 109 | |||
| 110 | $this->handleCustomFunction($objectMap, self::$afterUnify); |
||
| 111 | return $objectMap; |
||
| 112 | } |
||
| 113 | |||
| 114 | /** |
||
| 115 | * @param Map $objectModel |
||
| 116 | * |
||
| 117 | * @return void |
||
| 118 | */ |
||
| 119 | private function verify(Map $objectModel): void |
||
| 120 | { |
||
| 121 | $objectModel->isOK(); |
||
| 122 | $this->handleCustomFunction($objectModel, self::$afterVerify); |
||
| 123 | } |
||
| 124 | |||
| 125 | /** |
||
| 126 | * @param Map $objectModel |
||
| 127 | * @throws InvalidOperationException |
||
| 128 | * @throws \ReflectionException |
||
| 129 | */ |
||
| 130 | private function implement(Map $objectModel): void |
||
| 131 | { |
||
| 132 | /** @var SimpleMetadataProvider $meta */ |
||
| 133 | $meta = App::make('metadata'); |
||
| 134 | $namespace = $meta->getContainerNamespace() . '.'; |
||
| 135 | |||
| 136 | $entities = $objectModel->getEntities(); |
||
| 137 | foreach ($entities as $entity) { |
||
| 138 | /** @var class-string $className */ |
||
| 139 | $className = $entity->getClassName(); |
||
| 140 | /** @var string $entityName */ |
||
| 141 | $entityName = $entity->getName(); |
||
| 142 | $pluralName = Str::plural($entityName); |
||
| 143 | $entityType = $meta->addEntityType(new \ReflectionClass($className), $entityName, null, false, null); |
||
| 144 | if ($entityType->hasBaseType() !== false) { |
||
| 145 | throw new InvalidOperationException(''); |
||
| 146 | } |
||
| 147 | $entity->setOdataResourceType($entityType); |
||
| 148 | $this->implementProperties($entity); |
||
| 149 | $meta->addResourceSet($pluralName, $entityType); |
||
| 150 | $meta->oDataEntityMap[$className] = $meta->oDataEntityMap[$namespace . $entityName]; |
||
| 151 | } |
||
| 152 | $metaCount = count($meta->oDataEntityMap); |
||
| 153 | $entityCount = count($entities); |
||
| 154 | $expected = 2 * $entityCount; |
||
| 155 | if ($metaCount != $expected) { |
||
| 156 | $msg = 'Expected ' . $expected . ' items, actually got ' . $metaCount; |
||
| 157 | throw new InvalidOperationException($msg); |
||
| 158 | } |
||
| 159 | |||
| 160 | if (0 === count($objectModel->getAssociations())) { |
||
| 161 | return; |
||
| 162 | } |
||
| 163 | $assoc = $objectModel->getAssociations(); |
||
| 164 | $assoc = array_filter($assoc, function ($value) { |
||
| 165 | return $value instanceof AssociationMonomorphic; |
||
| 166 | }); |
||
| 167 | foreach ($assoc as $association) { |
||
| 168 | if (!$association->isOk()) { |
||
| 169 | throw new InvalidOperationException(''); |
||
| 170 | } |
||
| 171 | $this->implementAssociationsMonomorphic($objectModel, $association); |
||
| 172 | } |
||
| 173 | $this->handleCustomFunction($objectModel, self::$afterImplement); |
||
| 174 | } |
||
| 175 | |||
| 176 | /** |
||
| 177 | * @param Map $objectModel |
||
| 178 | * @param AssociationMonomorphic $associationUnderHammer |
||
| 179 | * @throws InvalidOperationException |
||
| 180 | * @throws \ReflectionException |
||
| 181 | */ |
||
| 182 | private function implementAssociationsMonomorphic( |
||
| 183 | Map $objectModel, |
||
| 184 | AssociationMonomorphic $associationUnderHammer |
||
| 185 | ): void { |
||
| 186 | /** @var SimpleMetadataProvider $meta */ |
||
| 187 | $meta = App::make('metadata'); |
||
| 188 | $first = $associationUnderHammer->getFirst(); |
||
| 189 | $last = $associationUnderHammer->getLast(); |
||
| 190 | $assocType = $associationUnderHammer->getAssociationType(); |
||
| 191 | $firstIsMany = (AssociationType::NULL_ONE_TO_MANY() == $assocType || AssociationType::ONE_TO_MANY() == $assocType) && |
||
|
0 ignored issues
–
show
introduced
by
Loading history...
|
|||
| 192 | ($first->getMultiplicity() == AssociationStubRelationType::MANY()); |
||
| 193 | |||
| 194 | $firstSide = $firstIsMany ? $last : $first; |
||
| 195 | $lastSide = $firstIsMany ? $first : $last; |
||
| 196 | |||
| 197 | /** @var ResourceEntityType $firstType */ |
||
| 198 | $firstType = $objectModel->getEntities()[$firstSide->getBaseType()]->getOdataResourceType(); |
||
| 199 | /** @var ResourceEntityType $secondType */ |
||
| 200 | $secondType = $objectModel->getEntities()[$lastSide->getBaseType()]->getOdataResourceType(); |
||
| 201 | |||
| 202 | $firstName = $firstSide->getRelationName(); |
||
| 203 | $lastName = $lastSide->getRelationName(); |
||
| 204 | |||
| 205 | switch ($assocType) { |
||
| 206 | case AssociationType::NULL_ONE_TO_NULL_ONE(): |
||
| 207 | case AssociationType::NULL_ONE_TO_ONE(): |
||
| 208 | case AssociationType::ONE_TO_ONE(): |
||
| 209 | $meta->addResourceReferenceSinglePropertyBidirectional( |
||
| 210 | $firstType, |
||
| 211 | $secondType, |
||
| 212 | $firstName, |
||
| 213 | $lastName |
||
| 214 | ); |
||
| 215 | break; |
||
| 216 | case AssociationType::NULL_ONE_TO_MANY(): |
||
| 217 | case AssociationType::ONE_TO_MANY(): |
||
| 218 | $meta->addResourceReferencePropertyBidirectional( |
||
| 219 | $firstType, |
||
| 220 | $secondType, |
||
| 221 | $firstName, |
||
| 222 | $lastName |
||
| 223 | ); |
||
| 224 | break; |
||
| 225 | case AssociationType::MANY_TO_MANY(): |
||
| 226 | $meta->addResourceSetReferencePropertyBidirectional( |
||
| 227 | $firstType, |
||
| 228 | $secondType, |
||
| 229 | $firstName, |
||
| 230 | $lastName |
||
| 231 | ); |
||
| 232 | } |
||
| 233 | } |
||
| 234 | |||
| 235 | /** |
||
| 236 | * @param EntityGubbins $unifiedEntity |
||
| 237 | * @throws InvalidOperationException |
||
| 238 | * @throws \ReflectionException |
||
| 239 | */ |
||
| 240 | private function implementProperties(EntityGubbins $unifiedEntity): void |
||
| 241 | { |
||
| 242 | /** @var SimpleMetadataProvider $meta */ |
||
| 243 | $meta = App::make('metadata'); |
||
| 244 | $odataEntity = $unifiedEntity->getOdataResourceType(); |
||
| 245 | $keyFields = $unifiedEntity->getKeyFields(); |
||
| 246 | /** @var EntityField[] $fields */ |
||
| 247 | $fields = array_diff_key($unifiedEntity->getFields(), $keyFields); |
||
| 248 | foreach ($keyFields as $keyField) { |
||
| 249 | $meta->addKeyProperty($odataEntity, $keyField->getName(), $keyField->getEdmFieldType()); |
||
| 250 | } |
||
| 251 | |||
| 252 | foreach ($fields as $field) { |
||
| 253 | if ($field->getPrimitiveType() == 'blob') { |
||
| 254 | $odataEntity->setMediaLinkEntry(true); |
||
| 255 | $streamInfo = new ResourceStreamInfo($field->getName()); |
||
| 256 | $odataEntity->addNamedStream($streamInfo); |
||
| 257 | continue; |
||
| 258 | } |
||
| 259 | |||
| 260 | $default = $field->getDefaultValue(); |
||
| 261 | $isFieldBool = TypeCode::BOOLEAN() == $field->getEdmFieldType(); |
||
| 262 | $default = $isFieldBool ? ($default ? 'true' : 'false') : strval($default); |
||
| 263 | |||
| 264 | $meta->addPrimitiveProperty( |
||
| 265 | $odataEntity, |
||
| 266 | $field->getName(), |
||
| 267 | $field->getEdmFieldType(), |
||
| 268 | $field->getFieldType() == EntityFieldType::PRIMITIVE_BAG(), |
||
| 269 | $default, |
||
| 270 | $field->getIsNullable() |
||
| 271 | ); |
||
| 272 | } |
||
| 273 | } |
||
| 274 | |||
| 275 | /** |
||
| 276 | * Bootstrap the application services. Post-boot. |
||
| 277 | * |
||
| 278 | * @throws InvalidOperationException |
||
| 279 | * @throws \ReflectionException |
||
| 280 | * @throws \Doctrine\DBAL\DBALException |
||
| 281 | * @throws \Exception |
||
| 282 | * @return void |
||
| 283 | */ |
||
| 284 | public function boot() |
||
| 285 | { |
||
| 286 | App::forgetInstance('metadata'); |
||
| 287 | App::forgetInstance('objectmap'); |
||
| 288 | $this->relationHolder = new MetadataRelationshipContainer(); |
||
| 289 | |||
| 290 | self::$metaNAMESPACE = env('ODataMetaNamespace', 'Data'); |
||
| 291 | // If we aren't migrated, there's no DB tables to pull metadata _from_, so bail out early |
||
| 292 | try { |
||
| 293 | if (!Schema::hasTable(strval(config('database.migrations')))) { |
||
| 294 | return; |
||
| 295 | } |
||
| 296 | } catch (\Exception $e) { |
||
| 297 | return; |
||
| 298 | } |
||
| 299 | |||
| 300 | $isCaching = true === $this->getIsCaching(); |
||
| 301 | $meta = Cache::get('metadata'); |
||
| 302 | $objectMap = Cache::get('objectmap'); |
||
| 303 | $hasCache = null != $meta && null != $objectMap; |
||
| 304 | |||
| 305 | if ($isCaching && $hasCache) { |
||
| 306 | App::instance('metadata', $meta); |
||
| 307 | App::instance('objectmap', $objectMap); |
||
| 308 | self::$isBooted = true; |
||
| 309 | return; |
||
| 310 | } |
||
| 311 | $meta = App::make('metadata'); |
||
| 312 | |||
| 313 | $modelNames = $this->getCandidateModels(); |
||
| 314 | $objectModel = $this->extract($modelNames); |
||
| 315 | $objectModel = $this->unify($objectModel); |
||
| 316 | $this->verify($objectModel); |
||
| 317 | $this->implement($objectModel); |
||
| 318 | $this->completedObjectMap = $objectModel; |
||
| 319 | $key = 'metadata'; |
||
| 320 | $objKey = 'objectmap'; |
||
| 321 | $this->handlePostBoot($isCaching, $hasCache, $key, $meta); |
||
| 322 | $this->handlePostBoot($isCaching, $hasCache, $objKey, $objectModel); |
||
| 323 | self::$isBooted = true; |
||
| 324 | } |
||
| 325 | |||
| 326 | /** |
||
| 327 | * Register the application services. Boot-time only. |
||
| 328 | * |
||
| 329 | * @return void |
||
| 330 | */ |
||
| 331 | public function register() |
||
| 332 | { |
||
| 333 | $this->app->/* @scrutinizer ignore-call */singleton('metadata', function () { |
||
| 334 | return new SimpleMetadataProvider('Data', self::$metaNAMESPACE); |
||
| 335 | }); |
||
| 336 | $this->app->/* @scrutinizer ignore-call */singleton('objectmap', function () { |
||
| 337 | return new Map(); |
||
| 338 | }); |
||
| 339 | } |
||
| 340 | |||
| 341 | /** |
||
| 342 | * @throws \Exception |
||
| 343 | * @return string[] |
||
| 344 | */ |
||
| 345 | protected function getCandidateModels(): array |
||
| 346 | { |
||
| 347 | return ClassReflectionHelper::getCandidateModels(); |
||
| 348 | } |
||
| 349 | |||
| 350 | /** |
||
| 351 | * @return IMetadataRelationshipContainer|null |
||
| 352 | */ |
||
| 353 | public function getRelationHolder(): ?IMetadataRelationshipContainer |
||
| 354 | { |
||
| 355 | return $this->relationHolder; |
||
| 356 | } |
||
| 357 | |||
| 358 | public function isRunningInArtisan(): bool |
||
| 359 | { |
||
| 360 | return App::runningInConsole() && !App::runningUnitTests(); |
||
| 361 | } |
||
| 362 | } |
||
| 363 |