schemaReady()) { return 0; } if ($this->has($title)) { $structure = $this->get($title); // Cache said it exists but the row is gone (deleted/cache drift): // fall through and recreate instead of dereferencing null. if ($structure !== null) { return $structure->id; } $this->clearCache($title); } $structureId = $this->repo->createStructure([ 'title' => $title, 'type' => $type, ]); // Add elements $elementIds = []; foreach ($elements as $element) { $elementId = $this->repo->addElement([ 'structureId' => $structureId, 'typeKey' => $element['typeKey'], 'label' => $element['label'], 'description' => $element['description'] ?? '', 'domainReference' => $element['domainReference'] ?? null, 'sortOrder' => $element['sortOrder'] ?? 0, 'meta' => $element['meta'] ?? null, ]); $elementIds[$element['typeKey']] = $elementId; ElementTypeRegistered::dispatch($structureId, $element['typeKey'], $element['label']); } // Add relationships (resolve typeKey → element ID) foreach ($relationships as $rel) { $fromId = $elementIds[$rel['fromTypeKey']] ?? null; $toId = $elementIds[$rel['toTypeKey']] ?? null; if ($fromId !== null && $toId !== null) { $this->repo->addRelationship([ 'structureId' => $structureId, 'fromElementId' => $fromId, 'toElementId' => $toId, 'relationshipType' => $rel['relationshipType'], 'description' => $rel['description'] ?? '', ]); } } $this->clearCache($title); StructureRegistered::dispatch($structureId, $title, $type); return $structureId; } /** * Register cross-structure mappings (idempotent per source element + target structure). * * @param int $sourceStructureId Source structure ID * @param int $targetStructureId Target structure ID * @param array $mappings Array of [{sourceTypeKey, targetTypeKey, mappingType}] * * @api */ public function registerMappings(int $sourceStructureId, int $targetStructureId, array $mappings): void { if (! $this->schemaReady()) { return; } foreach ($mappings as $mapping) { $sourceElement = $this->repo->getElementByTypeKey($sourceStructureId, $mapping['sourceTypeKey']); $targetElement = $this->repo->getElementByTypeKey($targetStructureId, $mapping['targetTypeKey']); if ($sourceElement === null || $targetElement === null) { continue; } if ($this->repo->mappingExists($sourceStructureId, $sourceElement->id, $targetStructureId)) { continue; } $this->repo->addMapping([ 'sourceStructureId' => $sourceStructureId, 'sourceElementId' => $sourceElement->id, 'targetStructureId' => $targetStructureId, 'targetElementId' => $targetElement->id, 'mappingType' => $mapping['mappingType'] ?? 'generates', ]); MappingCreated::dispatch($sourceStructureId, $targetStructureId, $mapping['mappingType'] ?? 'generates'); } } /** * Check if a structure is registered. * * @param string $title Structure title * * @api */ public function has(string $title): bool { if (! $this->schemaReady()) { return false; } return Cache::rememberForever(self::CACHE_PREFIX.'exists.'.$title, function () use ($title) { return $this->repo->structureExists($title); }); } /** * Get a registered structure by title (cached). * * @param string $title Structure title * * @api */ public function get(string $title): ?WorkStructure { if (! $this->schemaReady()) { return null; } return Cache::rememberForever(self::CACHE_PREFIX.'structure.'.$title, function () use ($title) { $structure = $this->repo->getStructureByTitle($title); if ($structure !== null && $structure->id !== null) { $structure->elements = $this->repo->getElements($structure->id); $structure->relationships = $this->repo->getRelationships($structure->id); } return $structure; }); } /** * Clear cache for a structure. * * @param string $title Structure title */ private function clearCache(string $title): void { Cache::forget(self::CACHE_PREFIX.'exists.'.$title); Cache::forget(self::CACHE_PREFIX.'structure.'.$title); } /** * Whether the WorkStructure schema has been created yet. * * Guards the boot-time plugin registration path against a fresh install or a * pending update where migration 30502 hasn't run. Cached per request so the * hasTable lookup doesn't repeat for every register()/has()/get() call. */ private function schemaReady(): bool { static $ready = null; if ($ready === null) { $ready = Schema::hasTable('zp_work_structures'); } return $ready; } }