222 lines
7.3 KiB
PHP
222 lines
7.3 KiB
PHP
<?php
|
|
|
|
namespace Leantime\Core\WorkStructure\Services;
|
|
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Illuminate\Support\Facades\Schema;
|
|
use Leantime\Core\Events\DispatchesEvents;
|
|
use Leantime\Core\WorkStructure\Events\ElementTypeRegistered;
|
|
use Leantime\Core\WorkStructure\Events\MappingCreated;
|
|
use Leantime\Core\WorkStructure\Events\StructureRegistered;
|
|
use Leantime\Core\WorkStructure\Models\WorkStructure;
|
|
use Leantime\Core\WorkStructure\Repositories\WorkStructureRepository;
|
|
|
|
/**
|
|
* Plugin registration service for work structures.
|
|
*
|
|
* Provides an idempotent API for plugins to register their structure
|
|
* definitions, elements, relationships, and cross-structure mappings.
|
|
*
|
|
* @api
|
|
*/
|
|
class StructureRegistry
|
|
{
|
|
use DispatchesEvents;
|
|
|
|
private const CACHE_PREFIX = 'workstructure.';
|
|
|
|
/**
|
|
* @param WorkStructureRepository $repo WorkStructure repository
|
|
*/
|
|
public function __construct(
|
|
private WorkStructureRepository $repo
|
|
) {}
|
|
|
|
/**
|
|
* Register a structure with elements and relationships (idempotent).
|
|
*
|
|
* @param string $title Structure title (unique)
|
|
* @param string $type 'system', 'plugin', 'custom'
|
|
* @param array $elements Array of element definitions [{typeKey, label, description?, domainReference?, sortOrder, meta?}]
|
|
* @param array $relationships Array of relationship defs [{fromTypeKey, toTypeKey, relationshipType, description?}]
|
|
* @return int Structure ID
|
|
*
|
|
* @api
|
|
*/
|
|
public function register(string $title, string $type, array $elements, array $relationships = []): int
|
|
{
|
|
// Plugins call this from boot-time register.php. During a fresh install or
|
|
// a pending update the WorkStructure tables may not exist yet — skip rather
|
|
// than crash boot; the plugin re-registers idempotently on the next request.
|
|
if (! $this->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;
|
|
}
|
|
}
|