OneBot: Leantime 改造版源码(BOM/Univer 表格/AI 接管/品牌替换等)

This commit is contained in:
wangruiguo
2026-09-03 18:49:20 +08:00
commit d647428529
3501 changed files with 1988906 additions and 0 deletions

View File

@@ -0,0 +1,28 @@
<?php
namespace Leantime\Core\WorkStructure\Events;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
/**
* Fired when element types are added to a structure.
*/
class ElementTypeRegistered
{
use Dispatchable, InteractsWithSockets, SerializesModels;
/**
* Create a new event instance.
*
* @param int $structureId The structure ID
* @param string $typeKey The element type key
* @param string $label The element label
*/
public function __construct(
public int $structureId,
public string $typeKey,
public string $label
) {}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace Leantime\Core\WorkStructure\Events;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
/**
* Fired when cross-structure mappings are defined.
*/
class MappingCreated
{
use Dispatchable, InteractsWithSockets, SerializesModels;
/**
* Create a new event instance.
*
* @param int $sourceStructureId The source structure ID
* @param int $targetStructureId The target structure ID
* @param string $mappingType The mapping type (generates, equivalent, informs)
*/
public function __construct(
public int $sourceStructureId,
public int $targetStructureId,
public string $mappingType
) {}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace Leantime\Core\WorkStructure\Events;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
/**
* Fired when a new work structure is registered.
*/
class StructureRegistered
{
use Dispatchable, InteractsWithSockets, SerializesModels;
/**
* Create a new event instance.
*
* @param int $structureId The registered structure ID
* @param string $title The structure title
* @param string $type The structure type (system, plugin, custom)
*/
public function __construct(
public int $structureId,
public string $title,
public string $type
) {}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace Leantime\Core\WorkStructure\Models;
/**
* ElementDefinition model — defines an element type within a structure
* (e.g., "milestone", "task", "goal").
*/
class ElementDefinition
{
public ?int $id = null;
public int $structureId = 0;
public string $typeKey = '';
public string $label = '';
public string $description = '';
public ?string $domainReference = null;
public int $sortOrder = 0;
public ?string $meta = null;
public ?string $createdAt = null;
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Leantime\Core\WorkStructure\Models;
/**
* RelationshipDefinition model — defines intra-structure relationships
* (e.g., "task belongs_to milestone").
*/
class RelationshipDefinition
{
public ?int $id = null;
public int $structureId = 0;
public int $fromElementId = 0;
public int $toElementId = 0;
public string $relationshipType = '';
public string $description = '';
public ?string $meta = null;
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Leantime\Core\WorkStructure\Models;
/**
* StructureMapping model — defines cross-structure element mappings
* (e.g., Logic Model "output" → Project "milestone").
*/
class StructureMapping
{
public ?int $id = null;
public int $sourceStructureId = 0;
public int $sourceElementId = 0;
public int $targetStructureId = 0;
public int $targetElementId = 0;
public string $mappingType = 'generates';
public ?string $meta = null;
}

View File

@@ -0,0 +1,31 @@
<?php
namespace Leantime\Core\WorkStructure\Models;
/**
* WorkStructure model — defines a structure type (e.g., "Project", "Logic Model").
*/
class WorkStructure
{
public ?int $id = null;
public string $title = '';
public string $description = '';
public string $type = 'custom';
public ?int $createdBy = null;
public ?string $meta = null;
public ?string $createdAt = null;
public ?string $modifiedAt = null;
/** @var ElementDefinition[] */
public array $elements = [];
/** @var RelationshipDefinition[] */
public array $relationships = [];
}

View File

@@ -0,0 +1,344 @@
<?php
namespace Leantime\Core\WorkStructure\Repositories;
use Illuminate\Database\ConnectionInterface;
use Leantime\Core\Db\Db as DbCore;
use Leantime\Core\Events\DispatchesEvents;
use Leantime\Core\WorkStructure\Models\ElementDefinition;
use Leantime\Core\WorkStructure\Models\RelationshipDefinition;
use Leantime\Core\WorkStructure\Models\StructureMapping;
use Leantime\Core\WorkStructure\Models\WorkStructure;
/**
* Repository for WorkStructure tables — CRUD via Laravel Query Builder.
*
* @api
*/
class WorkStructureRepository
{
use DispatchesEvents;
private ConnectionInterface $db;
/**
* @param DbCore $db Database connection wrapper
*/
public function __construct(DbCore $db)
{
$this->db = $db->getConnection();
}
// ─── Structures ──────────────────────────────────────────────────
/**
* Get a structure by ID.
*
* @param int $id Structure ID
*/
public function getStructure(int $id): ?WorkStructure
{
$row = $this->db->table('zp_work_structures')
->where('id', $id)
->first();
return $row ? $this->hydrateStructure($row) : null;
}
/**
* Get a structure by title.
*
* @param string $title Structure title
*/
public function getStructureByTitle(string $title): ?WorkStructure
{
$row = $this->db->table('zp_work_structures')
->where('title', $title)
->first();
return $row ? $this->hydrateStructure($row) : null;
}
/**
* Get all structures, optionally filtered by type.
*
* @param string|null $type Filter by type ('system', 'plugin', 'custom')
* @return WorkStructure[]
*/
public function getAllStructures(?string $type = null): array
{
$query = $this->db->table('zp_work_structures');
if ($type !== null) {
$query->where('type', $type);
}
return $query->get()
->map(fn ($row) => $this->hydrateStructure($row))
->all();
}
/**
* Create a new structure.
*
* @param array $values Structure data
* @return int Inserted ID
*/
public function createStructure(array $values): int
{
$now = now()->toDateTimeString();
return (int) $this->db->table('zp_work_structures')->insertGetId([
'title' => $values['title'],
'description' => $values['description'] ?? '',
'type' => $values['type'] ?? 'custom',
'created_by' => $values['createdBy'] ?? null,
'meta' => isset($values['meta']) ? json_encode($values['meta']) : null,
'created_at' => $now,
'modified_at' => $now,
]);
}
/**
* Check if a structure exists by title.
*
* @param string $title Structure title
*/
public function structureExists(string $title): bool
{
return $this->db->table('zp_work_structures')
->where('title', $title)
->exists();
}
// ─── Elements ────────────────────────────────────────────────────
/**
* Get all elements for a structure.
*
* @param int $structureId Structure ID
* @return ElementDefinition[]
*/
public function getElements(int $structureId): array
{
return $this->db->table('zp_work_structure_elements')
->where('structure_id', $structureId)
->orderBy('sort_order')
->get()
->map(fn ($row) => $this->hydrateElement($row))
->all();
}
/**
* Get an element by structure ID and type key.
*
* @param int $structureId Structure ID
* @param string $typeKey Element type key
*/
public function getElementByTypeKey(int $structureId, string $typeKey): ?ElementDefinition
{
$row = $this->db->table('zp_work_structure_elements')
->where('structure_id', $structureId)
->where('type_key', $typeKey)
->first();
return $row ? $this->hydrateElement($row) : null;
}
/**
* Add an element to a structure.
*
* @param array $values Element data
* @return int Inserted ID
*/
public function addElement(array $values): int
{
return (int) $this->db->table('zp_work_structure_elements')->insertGetId([
'structure_id' => $values['structureId'],
'type_key' => $values['typeKey'],
'label' => $values['label'],
'description' => $values['description'] ?? '',
'domain_reference' => $values['domainReference'] ?? null,
'sort_order' => $values['sortOrder'] ?? 0,
'meta' => isset($values['meta']) ? json_encode($values['meta']) : null,
'created_at' => now()->toDateTimeString(),
]);
}
// ─── Relationships ───────────────────────────────────────────────
/**
* Get all relationships for a structure.
*
* @param int $structureId Structure ID
* @return RelationshipDefinition[]
*/
public function getRelationships(int $structureId): array
{
return $this->db->table('zp_work_structure_relationships')
->where('structure_id', $structureId)
->get()
->map(fn ($row) => $this->hydrateRelationship($row))
->all();
}
/**
* Add an intra-structure relationship.
*
* @param array $values Relationship data
* @return int Inserted ID
*/
public function addRelationship(array $values): int
{
return (int) $this->db->table('zp_work_structure_relationships')->insertGetId([
'structure_id' => $values['structureId'],
'from_element_id' => $values['fromElementId'],
'to_element_id' => $values['toElementId'],
'relationship_type' => $values['relationshipType'],
'description' => $values['description'] ?? '',
'meta' => isset($values['meta']) ? json_encode($values['meta']) : null,
]);
}
// ─── Mappings ────────────────────────────────────────────────────
/**
* Get all mappings between two structures.
*
* @param int $sourceStructureId Source structure ID
* @param int $targetStructureId Target structure ID
* @return StructureMapping[]
*/
public function getMappings(int $sourceStructureId, int $targetStructureId): array
{
return $this->db->table('zp_work_structure_mappings')
->where('source_structure_id', $sourceStructureId)
->where('target_structure_id', $targetStructureId)
->get()
->map(fn ($row) => $this->hydrateMapping($row))
->all();
}
/**
* Add a cross-structure mapping.
*
* @param array $values Mapping data
* @return int Inserted ID
*/
public function addMapping(array $values): int
{
return (int) $this->db->table('zp_work_structure_mappings')->insertGetId([
'source_structure_id' => $values['sourceStructureId'],
'source_element_id' => $values['sourceElementId'],
'target_structure_id' => $values['targetStructureId'],
'target_element_id' => $values['targetElementId'],
'mapping_type' => $values['mappingType'] ?? 'generates',
'meta' => isset($values['meta']) ? json_encode($values['meta']) : null,
]);
}
/**
* Get distinct target structure IDs that have mappings from a given source.
*
* @param int $sourceStructureId Source structure ID
* @return int[]
*/
public function getTargetStructureIds(int $sourceStructureId): array
{
return $this->db->table('zp_work_structure_mappings')
->where('source_structure_id', $sourceStructureId)
->distinct()
->pluck('target_structure_id')
->map(fn ($id) => (int) $id)
->all();
}
/**
* Check if a mapping exists.
*
* @param int $sourceStructureId Source structure ID
* @param int $sourceElementId Source element ID
* @param int $targetStructureId Target structure ID
*/
public function mappingExists(int $sourceStructureId, int $sourceElementId, int $targetStructureId): bool
{
return $this->db->table('zp_work_structure_mappings')
->where('source_structure_id', $sourceStructureId)
->where('source_element_id', $sourceElementId)
->where('target_structure_id', $targetStructureId)
->exists();
}
// ─── Hydrators ───────────────────────────────────────────────────
/**
* Hydrate a WorkStructure model from a database row.
*/
private function hydrateStructure(object $row): WorkStructure
{
$model = new WorkStructure;
$model->id = (int) $row->id;
$model->title = $row->title;
$model->description = $row->description ?? '';
$model->type = $row->type;
$model->createdBy = $row->created_by ? (int) $row->created_by : null;
$model->meta = $row->meta;
$model->createdAt = $row->created_at;
$model->modifiedAt = $row->modified_at;
return $model;
}
/**
* Hydrate an ElementDefinition model from a database row.
*/
private function hydrateElement(object $row): ElementDefinition
{
$model = new ElementDefinition;
$model->id = (int) $row->id;
$model->structureId = (int) $row->structure_id;
$model->typeKey = $row->type_key;
$model->label = $row->label;
$model->description = $row->description ?? '';
$model->domainReference = $row->domain_reference;
$model->sortOrder = (int) $row->sort_order;
$model->meta = $row->meta;
$model->createdAt = $row->created_at;
return $model;
}
/**
* Hydrate a RelationshipDefinition model from a database row.
*/
private function hydrateRelationship(object $row): RelationshipDefinition
{
$model = new RelationshipDefinition;
$model->id = (int) $row->id;
$model->structureId = (int) $row->structure_id;
$model->fromElementId = (int) $row->from_element_id;
$model->toElementId = (int) $row->to_element_id;
$model->relationshipType = $row->relationship_type;
$model->description = $row->description ?? '';
$model->meta = $row->meta;
return $model;
}
/**
* Hydrate a StructureMapping model from a database row.
*/
private function hydrateMapping(object $row): StructureMapping
{
$model = new StructureMapping;
$model->id = (int) $row->id;
$model->sourceStructureId = (int) $row->source_structure_id;
$model->sourceElementId = (int) $row->source_element_id;
$model->targetStructureId = (int) $row->target_structure_id;
$model->targetElementId = (int) $row->target_element_id;
$model->mappingType = $row->mapping_type;
$model->meta = $row->meta;
return $model;
}
}

View File

@@ -0,0 +1,170 @@
<?php
namespace Leantime\Core\WorkStructure\Services;
use Leantime\Core\Events\DispatchesEvents;
use Leantime\Core\WorkStructure\Models\WorkStructure;
use Leantime\Core\WorkStructure\Repositories\WorkStructureRepository;
/**
* Cross-structure mapping query service.
*
* Resolves element type mappings between different work structures
* (e.g., Logic Model "output" → Project "milestone").
*
* @api
*/
class MappingService
{
use DispatchesEvents;
/**
* @param WorkStructureRepository $repo WorkStructure repository
*/
public function __construct(
private WorkStructureRepository $repo
) {}
/**
* Get all mappings between two structures.
*
* @param int $sourceStructureId Source structure ID
* @param int $targetStructureId Target structure ID
* @return \Leantime\Core\WorkStructure\Models\StructureMapping[]
*
* @api
*/
public function getMappings(int $sourceStructureId, int $targetStructureId): array
{
return $this->repo->getMappings($sourceStructureId, $targetStructureId);
}
/**
* Get the target element type key for a given source element type key.
*
* @param int $sourceStructureId Source structure ID
* @param string $sourceTypeKey Source element type key
* @param int $targetStructureId Target structure ID
* @return string|null Target element type key, or null if no mapping exists
*
* @api
*/
public function getTargetElementKey(int $sourceStructureId, string $sourceTypeKey, int $targetStructureId): ?string
{
$sourceElement = $this->repo->getElementByTypeKey($sourceStructureId, $sourceTypeKey);
if ($sourceElement === null) {
return null;
}
$mappings = $this->repo->getMappings($sourceStructureId, $targetStructureId);
// Resolve target element IDs → typeKeys with a single fetch instead of
// re-querying all target elements inside the mappings loop.
$targetIdToKey = [];
foreach ($this->repo->getElements($targetStructureId) as $el) {
$targetIdToKey[$el->id] = $el->typeKey;
}
foreach ($mappings as $mapping) {
if ($mapping->sourceElementId === $sourceElement->id) {
return $targetIdToKey[$mapping->targetElementId] ?? null;
}
}
return null;
}
/**
* Get all target structures that have mappings from a given source structure.
*
* Each returned structure includes its elements populated.
*
* @param int $sourceStructureId Source structure ID
* @return WorkStructure[]
*
* @api
*/
public function getTargetStructures(int $sourceStructureId): array
{
$targetIds = $this->repo->getTargetStructureIds($sourceStructureId);
$structures = [];
foreach ($targetIds as $targetId) {
$structure = $this->repo->getStructure($targetId);
if ($structure !== null) {
$structure->elements = $this->repo->getElements($targetId);
$structures[] = $structure;
}
}
return $structures;
}
/**
* Get target element type keys that have mappings from the source structure.
*
* @param int $sourceStructureId Source structure ID
* @param int $targetStructureId Target structure ID
* @return string[] Array of target element typeKeys (e.g., ['milestone', 'task', 'goal'])
*
* @api
*/
public function getMappedElementKeys(int $sourceStructureId, int $targetStructureId): array
{
$mappings = $this->repo->getMappings($sourceStructureId, $targetStructureId);
$targetElements = $this->repo->getElements($targetStructureId);
$elementIdToKey = [];
foreach ($targetElements as $el) {
$elementIdToKey[$el->id] = $el->typeKey;
}
$keys = [];
foreach ($mappings as $mapping) {
$key = $elementIdToKey[$mapping->targetElementId] ?? null;
if ($key !== null && ! in_array($key, $keys, true)) {
$keys[] = $key;
}
}
return $keys;
}
/**
* Get the source element type key for a given target element type key.
*
* @param int $targetStructureId Target structure ID
* @param string $targetTypeKey Target element type key
* @param int $sourceStructureId Source structure ID
* @return string|null Source element type key, or null if no mapping exists
*
* @api
*/
public function getSourceElementKey(int $targetStructureId, string $targetTypeKey, int $sourceStructureId): ?string
{
$targetElement = $this->repo->getElementByTypeKey($targetStructureId, $targetTypeKey);
if ($targetElement === null) {
return null;
}
$mappings = $this->repo->getMappings($sourceStructureId, $targetStructureId);
// Resolve source element IDs → typeKeys with a single fetch instead of
// re-querying all source elements inside the mappings loop.
$sourceIdToKey = [];
foreach ($this->repo->getElements($sourceStructureId) as $el) {
$sourceIdToKey[$el->id] = $el->typeKey;
}
foreach ($mappings as $mapping) {
if ($mapping->targetElementId === $targetElement->id) {
return $sourceIdToKey[$mapping->sourceElementId] ?? null;
}
}
return null;
}
}

View File

@@ -0,0 +1,221 @@
<?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;
}
}

View File

@@ -0,0 +1,129 @@
<?php
namespace Leantime\Core\WorkStructure\Services;
use Leantime\Core\Events\DispatchesEvents;
use Leantime\Core\WorkStructure\Models\ElementDefinition;
use Leantime\Core\WorkStructure\Models\RelationshipDefinition;
use Leantime\Core\WorkStructure\Models\WorkStructure;
use Leantime\Core\WorkStructure\Repositories\WorkStructureRepository;
/**
* CRUD service for work structures, elements, and relationships.
*
* @api
*/
class WorkStructureService
{
use DispatchesEvents;
/**
* @param WorkStructureRepository $repo WorkStructure repository
*/
public function __construct(
private WorkStructureRepository $repo
) {}
/**
* Get a structure by ID with its elements and relationships.
*
* @param int $id Structure ID
*
* @api
*/
public function getStructure(int $id): ?WorkStructure
{
$structure = $this->repo->getStructure($id);
if ($structure !== null) {
$structure->elements = $this->repo->getElements($id);
$structure->relationships = $this->repo->getRelationships($id);
}
return $structure;
}
/**
* Get a structure by title with its elements and relationships.
*
* @param string $title Structure title
*
* @api
*/
public function getStructureByTitle(string $title): ?WorkStructure
{
$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;
}
/**
* Get all elements for a structure, ordered by sort_order.
*
* @param int $structureId Structure ID
* @return ElementDefinition[]
*
* @api
*/
public function getElements(int $structureId): array
{
return $this->repo->getElements($structureId);
}
/**
* Get all intra-structure relationships.
*
* @param int $structureId Structure ID
* @return RelationshipDefinition[]
*
* @api
*/
public function getRelationships(int $structureId): array
{
return $this->repo->getRelationships($structureId);
}
/**
* Create a new structure.
*
* @param array $values Structure data (title, description, type, createdBy, meta)
* @return int Created structure ID
*
* @api
*/
public function createStructure(array $values): int
{
return $this->repo->createStructure($values);
}
/**
* Add an element to a structure.
*
* @param array $values Element data (structureId, typeKey, label, description, domainReference, sortOrder, meta)
* @return int Created element ID
*
* @api
*/
public function addElement(array $values): int
{
return $this->repo->addElement($values);
}
/**
* Add an intra-structure relationship.
*
* @param array $values Relationship data (structureId, fromElementId, toElementId, relationshipType, description, meta)
* @return int Created relationship ID
*
* @api
*/
public function addRelationship(array $values): int
{
return $this->repo->addRelationship($values);
}
}

View File

@@ -0,0 +1,66 @@
<?php
namespace Leantime\Core\WorkStructure;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\ServiceProvider;
use Leantime\Core\WorkStructure\Repositories\WorkStructureRepository;
use Leantime\Core\WorkStructure\Services\MappingService;
use Leantime\Core\WorkStructure\Services\StructureRegistry;
use Leantime\Core\WorkStructure\Services\WorkStructureService;
/**
* Registers the WorkStructure infrastructure and seeds the built-in
* "Project" system structure.
*
* WorkStructure is a meta-model that describes how Leantime entities are
* composed and how they map across structures (e.g., a Logic Model "output"
* generates a Project "milestone"). It lives in Core — like Plugins and Auth —
* because it governs domains rather than being one. Plugins register their own
* structures and mappings through {@see StructureRegistry}.
*/
class WorkStructureServiceProvider extends ServiceProvider
{
/**
* Bind the WorkStructure services as singletons.
*/
public function register(): void
{
$this->app->singleton(WorkStructureRepository::class);
$this->app->singleton(StructureRegistry::class);
$this->app->singleton(MappingService::class);
$this->app->singleton(WorkStructureService::class);
}
/**
* Seed the built-in "Project" structure (idempotent).
*
* StructureRegistry::register() is cache-guarded and a no-op once the
* structure exists, so this costs a cache read in steady state. Wrapped in
* try/catch because the tables do not exist yet during a fresh install.
*/
public function boot(): void
{
try {
/** @var StructureRegistry $registry */
$registry = $this->app->make(StructureRegistry::class);
$registry->register(
'Project',
'system',
[
['typeKey' => 'milestone', 'label' => 'Milestone', 'domainReference' => 'Leantime\\Domain\\Tickets', 'sortOrder' => 1],
['typeKey' => 'task', 'label' => 'Task', 'domainReference' => 'Leantime\\Domain\\Tickets', 'sortOrder' => 2],
['typeKey' => 'goal', 'label' => 'Goal', 'domainReference' => 'Leantime\\Domain\\Goalcanvas', 'sortOrder' => 3],
],
[
['fromTypeKey' => 'task', 'toTypeKey' => 'milestone', 'relationshipType' => 'belongs_to'],
['fromTypeKey' => 'milestone', 'toTypeKey' => 'goal', 'relationshipType' => 'measures'],
]
);
} catch (\Throwable $e) {
// Tables may not exist yet during install/update — degrade gracefully.
Log::debug('WorkStructure seed skipped: '.$e->getMessage());
}
}
}