본문으로 건너뛰기
참조

Contribution 계약

모든 SDK contribution 인터페이스와 필수 메서드, 그리고 contributor 클래스가 발견되기 위해 만족해야 하는 규칙입니다.

Contribution 계약

Manifest 탐색 대상 클래스에서 인터페이스를 구현해 App 기능을 게시합니다. 아래 예시는 Resource 식별자 등록이고, 다른 기능의 인터페이스는 목록에서 찾을 수 있습니다. 클래스의 명시적인 implements와 App Manifest 계약의 탐색 위치가 모두 있어야 발견됩니다.

최소 식별자 Contribution

먼저 Resource 만들기로 Amuzcorp\Nexia\Workshop\Models\Note를 생성하세요. 아래 완전한 클래스는 Resource 식별자 인터페이스만 보여 주며 기반 클래스나 trait가 필요하지 않습니다.

코드 예시
PHP
<?php

declare(strict_types=1);

namespace Amuzcorp\Nexia\Workshop\Contribution\Resources;

use Amuzcorp\Nexia\Workshop\Models\Note;
use Nexia\Contribution\Contracts\ResourceCatalogContribution;

final class NoteCatalog implements ResourceCatalogContribution
{
    public static function resourceKey(): string
    {
        return 'workshop.note';
    }

    public static function resourceModelClass(): string
    {
        return Note::class;
    }
}

생성된 NoteModule에는 이 식별자와 함께 permission, 권한, descriptor, 내비게이션이 있습니다. 실제 App에서는 생성된 module을 유지하세요. 위 NoteCatalog를 함께 등록하거나 이 식별자 예시로 전체 module을 교체하지 않습니다. Resource key에는 소유자가 하나여야 합니다.

클래스의 인터페이스 선언과 Manifest 탐색 위치가 모두 있어야 발견됩니다. 메서드를 제공하는 trait만으로 인터페이스가 선언되지는 않습니다.

Contribution 인터페이스

인터페이스네임스페이스필수 메서드
ResourceCatalogContributionNexia\Contribution\ContractsresourceKey(), resourceModelClass()
ResourceAuthorizationContributionNexia\Contribution\ContractsresourceAuthorization()
ShellResourceContributionNexia\AppRuntime\ContractsshellResource()
PermissionContributionNexia\Permission\ContractscatalogPermissionDefinitions()
RolePresetContributionNexia\Permission\ContractsrolePresets()
LegalEntityMemberSelfAccessContributionNexia\Permission\ContractslegalEntityMemberSelfAccessKeys()
NavigationContributionNexia\Navigation\ContractsnavigationItems()
PaletteCommandContributionNexia\Palette\ContractspaletteCommandItems()
InheritedSearchVisibilityContributionNexia\Contribution\ContractsinheritedSearchVisibilityRelation()과 Resource Catalog 정체성
DashboardWidgetContributionNexia\Dashboard\ContractsdashboardWidgets()
DashboardQuerySourceContributionNexia\Dashboard\ContractsdashboardQuerySources()
AgentToolContributionNexia\Agent\ContractsagentTools()
AppDescriptorContributionNexia\AppDescriptors\ContractsappDescriptors(): AppDescriptorSet
ResourceSummaryContributionNexia\AppDescriptors\ContractsresourceKey(), summarize()
BatchResourceSummaryContributionNexia\AppDescriptors\ContractsresourceKey(), summarize(), summarizeMany()
ResourceReferenceResolutionContributionNexia\ResourceReference\ContractsresourceKey(), resolve()
ResourceReferenceBatchResolutionContributionNexia\ResourceReference\Contractsresolution 메서드와 resolveMany()
ResourceReferenceSearchContributionNexia\ResourceReference\Contractssearch(), 그리고 해석 메서드들
ResourceReferenceEffectiveRangeSearchContributionNexia\ResourceReference\Contractssearch·resolution 메서드와 searchEffectiveRange()
ResourceReferencePagedSearchContributionNexia\ResourceReference\ContractsresourceKey(), resolve(), search(), authorized(), searchPage()
CompositionQueryContributionNexia\ResourceComposition\ContractscompositionQueryProvider(), resourceKey(), resourceModelClass()
ResourceTransferExportSourceContributionNexia\ResourceTransfer\ContractsresourceTransferExportSources()
ImportRecipeContributionNexia\ResourceImport\ContractsresourceImportRecipes()
ResourceImportPipelineContributionNexia\ResourceImport\ContractsresourceImportPipelines()
SignatureDocumentDataSourceContributionNexia\Signature\ContractsappDescriptors(), signatureDocumentDataSourceProviders()
SignatureBulkBindingContributionNexia\Signature\ContractsappDescriptors(), signatureBulkBindingProviders()
CopyableTemplateContributionNexia\Templates\Contractstemplates(), apply()
FixtureContributionNexia\Fixture\ContractsappKey(), fixtureKeys(), seed()
SetupTaskContributionNexia\Setup\Contractstasks()
FeatureGuideContributionNexia\Guidance\ContractsownerKey(), guides()
SelfWorkContextContributionNexia\SelfService\ContractsworkContexts()
SelfServiceActionItemContributionNexia\SelfService\ContractsselfServiceActions()

Contribution은 labelKey, titleKey, descriptionKey 같은 정확한 카탈로그 키만 담고, 호스트가 payload 경계에서 요청 로케일에 맞춰 해석합니다. 백엔드 contribution은 로케일 맵을 불러오는 대신 키를 제공합니다.

대부분의 메서드가 static입니다. 모든 typed descriptor는 하나의 static publication 경계인 AppDescriptorContribution::appDescriptors()에서 immutable set으로 게시하며, 그 set에는 Resource, Slot Widget, Approval, Signature, Process, Decision, Reporting descriptor를 함께 담을 수 있습니다. Descriptor key는 비어 있지 않아야 하고 앞뒤 공백이 없어야 합니다. Core는 contributor FQCN 순서와 각 set의 선언 순서를 차례로 보존하고, 호스트 전용 테스트 등록은 호출 순서대로 그 뒤에 둡니다. ResourceSummaryContribution, ResourceReference 인터페이스, CopyableTemplateContribution, FixtureContribution은 요청마다 또는 실행마다 해석·적용·시딩하므로 인스턴스 메서드를 씁니다. SetupTaskContribution도 인스턴스 기반입니다. 호스트가 컨테이너에서 contributor를 해석하고 Manifest 발견 기록에서 소유자를 추론한 뒤 반환된 선언을 등록합니다. 두 Signature contribution은 appDescriptors()로 정적 descriptor를, 두 번째 메서드로 instance runtime provider를 게시합니다. Core는 descriptor/provider의 정확한 identity를 연결하고 provider가 없으면 fail-closed합니다.

ResourceSummaryContribution은 Spotlight 같은 compact 호스트 화면에서 쓰는 레코드 표현 경계이기도 합니다. 소유 App은 권한 검사를 마친 display, 선택적 route, 선택적 Shell 아이콘 이름, typed field를 반환합니다. Field에 ResourceSummaryFieldRole::Subtitle, Meta, Status를 지정하면 compact consumer가 의미상 배치 위치를 알 수 있지만, React 컴포넌트와 간격, 말줄임, 접근성은 계속 호스트가 소유합니다. 여러 resource ID를 한 번의 제한된 쿼리로 요약할 수 있으면 BatchResourceSummaryContribution을 구현합니다. 기존 provider에는 Core가 단건 호출 fallback을 유지합니다. Spotlight는 검색 후보를 DB에서 다시 조회하고 권한을 확인한 뒤에만 ResourceVisibility::Search를 전달하며, 소유자는 여전히 레코드 가시성을 검사하고 보이지 않는 레코드에는 null을 반환해야 합니다.

App 소유 Agent 도구

일반 Resource 데이터, Dashboard query source, 기존 화면 action으로 표현할 수 없는 기능에만 AgentToolContribution을 사용합니다. App은 권한이 적용된 평범한 Laravel route를 소유하고 불변 metadata만 게시합니다.

코드 예시
PHP
use Nexia\Agent\Contracts\AgentToolContribution;
use Nexia\Agent\AgentToolDeclaration;
use Nexia\Agent\AgentToolLane;
use Nexia\Agent\AgentToolRouting;
use Nexia\Agent\AgentToolSurface;
use Nexia\Agent\AgentToolTier;

final class TimeAbsenceAgentTools implements AgentToolContribution
{
    public static function agentTools(): array
    {
        return [new AgentToolDeclaration(
            key: 'time-absence.personal_time_leave.read',
            tier: AgentToolTier::Auto,
            routing: AgentToolRouting::read(
                AgentToolLane::DirectData,
                AgentToolSurface::Data,
            ),
            description: 'Read the current user personal leave balance.',
            method: 'GET',
            path: '/api/legal-entities/{legalEntity:public_id}/time-absence/personal-leave',
            permissions: ['time-absence.leave_balance.read'],
        )];
    }
}

Key의 첫 segment는 안정적인 App namespace이며 kebab-case를 쓸 수 있습니다. 뒤의 dotted segment는 모두 소문자 영숫자와 underscore만 사용합니다. permissions는 비어 있을 수 없고 실제 route guard를 정확히 나타내야 합니다. inputSchema를 선언하면 비어 있지 않은 JSON Schema object여야 합니다. Laravel scoped placeholder인 {legalEntity:public_id}는 route 문법 그대로 선언합니다. Gateway가 호출 전에 현재 Legal Entity 또는 placeholder 이름과 정확히 일치하는 인자로 치환합니다. 일반 id 인자는 {user}, {ticket} 등 다른 이름의 placeholder를 대신하지 않습니다.

Precondition은 자유 형식 tag가 아니라 Router가 해석하는 정확한 capability입니다. legal_entity_selected는 신뢰된 delegation context에 활성 Legal Entity가 있을 때만 충족되며, 없으면 Router가 도구를 노출하지 않습니다. 지원하지 않는 precondition도 도구를 계속 비노출 상태로 둡니다.

발견 과정은 contributor를 Core container에서 해석하지 않고 static 메서드를 호출합니다. 선언할 수 있는 것은 Laravel route binding, routing의 lane/effect/surface, 기본 확인 tier, permission metadata, invalidation hint뿐입니다. Gateway handler, Closure, Core Action, recipe, host widget, browser tool은 게시할 수 없습니다. External effect에는 AgentToolTier::Confirm과 external-share facet도 필요합니다.

이 확장은 고유한 업무 기능에 사용합니다. 일반 Resource CRUD와 선언된 업무 동작은 Resource descriptor와 고정된 범용 데이터 도구를 사용하며 AgentToolContribution이 필요하지 않습니다.

인터페이스 상속

네 인터페이스가 ResourceCatalogContribution을 확장하므로, 그것들을 구현하면 resourceKey()와 resourceModelClass()도 필요합니다.

인터페이스추가로 필요한 것
ResourceAuthorizationContributionresourceKey(), resourceModelClass()
ShellResourceContributionresourceKey(), resourceModelClass()
DashboardWidgetContributionresourceKey(), resourceModelClass()
InheritedSearchVisibilityContributionresourceKey(), resourceModelClass()

따라서 대시보드 위젯 contributor는 설계상 Resource에 묶입니다.

메서드를 공급하는 트레이트

트레이트네임스페이스구현하는 것읽어오는 곳
ContributesResourcePermissionsNexia\Permission\ConcernscatalogPermissionDefinitions()permissionResources()
ContributesNavigationDestinationNexia\Navigation\ConcernsnavigationItems()$navigation
ContributesNavigationDestinationActionsNexia\Navigation\Concerns에이전트 동작 면agentNavigationActionDefinitions()
HasShellResourceNexia\AppRuntime\ConcernsshellResource()Catalog 정체성

인터페이스는 여전히 여러분이 선언합니다. implements PermissionContribution 없이 ContributesResourcePermissions를 쓰면 메서드는 동작하지만 발견이 절대 선택하지 않는 클래스가 되고, doctor가 permission contributors: 0 classes를 보고합니다.

Contributor가 만족해야 하는 요건

요건이유
선언된 contributionLocations() root 안manifest 생성이 그 directory만 색인
abstract가 아님추상 클래스는 건너뜀
파일 이름이 클래스 이름과 일치PSR-4 오토로딩
키가 App 접두를 달고 안정적임키가 권한 행과 번역, 저장된 테넌트 상태에 나타남
라벨이 App 로케일 카탈로그에서 옴리터럴 문자열은 번역되지 않은 채로 배포됨
발견 중에 테넌트 쓰기 없음레지스트리는 행위자나 테넌트 컨텍스트가 존재하기 전에 조립됨
반복 호출해도 안전소비 registry가 선언을 두 번 이상 읽을 수 있음

마지막 둘이 가장 중요합니다. SetupTaskContribution::tasks()를 포함한 선언 메서드는 레지스트리가 조립되는 동안 실행됩니다. 이때는 초기화된 테넌트도 없고 알려진 행위자도 없습니다. 데이터베이스를 읽거나 현재 사용자를 해석하거나 무엇이든 쓰면 실패하거나 레지스트리를 망가뜨립니다. Setup evaluator는 별도 단계입니다. 호스트가 테넌트 plan을 만들 때만 evaluate()를 호출하고 불투명한 테넌트·행위자·로케일·시간 컨텍스트를 제공합니다. Contributor는 여전히 발견 단계에서 테넌트 작업을 하지 않습니다.

결과 또는 반환: Descriptor 값 객체

AppDescriptorContribution은 타입 있는 객체의 immutable AppDescriptorSet을 돌려주며, 객체는 전부 Nexia\AppDescriptors에 있습니다. 호스트의 AppDescriptorCatalog는 이 단일 계약을 발견하고 contributor provenance를 보존한 뒤 소비자가 필요한 descriptor class만 요청하게 합니다.

클래스필수 생성자 인자
ResourceDescriptorkey, version; 리소스 자체를 가리키는 선택적 labelKey
EventDescriptor전체 Event key, 양의 정수 schemaVersion, aggregateType, payloadSchema
SlotWidgetDescriptorkey, version, slot, component, slotApiVersion
ApprovalFormBindingDescriptorappKey, resourceKey, actionKey, labelKey, entryModes, formWidgetKey, documentSchemaKey
ApprovalDocumentSchemaappKey, resourceKey, sections
ApprovalBusinessTemplatePresetDescriptorApp 소유 preset key와 business-template 필드
ApprovalRoutePolicyPresetDescriptorApp 소유 preset key와 route-policy 필드
SignatureTemplateBindingDescriptorApp 소유 signature-template binding 필드
ProcessStartBindingDescriptorappKey, bindingKey, definitionKey, processId, resourceKey, startPermissionKey, labelKey
ProcessWorkActionDescriptorkind, appKey, actionKey, labelKey, topic; 선택 payload/input, approvalTask, outputContract 메타데이터
ProcessApprovalTaskConfigurationbindingKey, outcomeTopic; 선택 outcomes, payload 키, businessTemplatePresetKey, routePolicyPresetKey
ProcessUserTaskFormDescriptorkey, appKey, labelKey
ProcessTemplateDescriptorkey, version, appKey(nullable이지만 기본값 없음), labelKey, category, structure
DecisionResultTemplateDescriptorkey, version, labelKey, fields; 선택 descriptionKey, decisionTable, hitPolicy
ReportingViewDescriptorkey, version, viewName, columns

버전이 있는 최상위 descriptor는 status: DescriptorStatus를 가지며 보통 Active가 기본값이고 다른 상태는 Deprecated와 Removed입니다. 대부분 문자열 version을 쓰지만 EventDescriptor는 EventEnvelope에 실리는 양의 정수 schemaVersion을 요구합니다.

이 descriptor type들은 계속 서로 구분되는 typed 값입니다. generic set은 각각의 discovery interface와 registry 진입점만 대체하며 Approval preset, Signature binding, Public Event 계약, 어떤 Process/DMN 기능도 제거하지 않습니다.

Descriptor 생성자는 자기 값을 즉시 검증합니다. 엄격한 호스트 게이트도 각 set의 내용을 검사해 선언한 descriptor 타입, App 소유 key prefix, 계열별 key 유일성을 검사합니다. 독립 공개 integration Event는 EventDescriptor로 선언하고, 공개 Resource 라이프사이클 Event는 ResourceDescriptor 아래에 둔 채 두 번째로 선언하지 않습니다. EventPayloadSchema는 지원하는 scalar·array·object type만 받고, required: false가 아니면 필드를 필수로 보며, 선언하지 않은 payload 필드를 거부합니다. 조합 가능한 payload schema에는 PublicEventPayloadSchema의 라벨 규칙도 적용합니다. 경로별 번역 키를 파생할 수 있다면 PublicEventPayloadSchema::withLabelKeys()를 쓰세요.

선택 App의 잘못된 contribution은 레지스트리 조립 중 격리되어 관련 없는 App의 부팅을 막지 않습니다. 하지만 이것은 승인 게이트가 아닙니다. nexia-apps:validate-app-descriptors, App 활성화, 테넌트 설치는 모두 엄격 검증을 실행하고, 동기화나 테넌트 상태 쓰기 전에 잘못된 App을 거부합니다.

오류

증상원인해결
WARN contribution locations: no package contribution location is registered디렉터리 미선언 또는 네임스페이스 불일치contributionLocations() 수정
WARN permission contributors: 0 classes인터페이스 선언 없이 트레이트만 사용implements PermissionContribution 추가
WARN permissions discovered: 0 permission definitions발견된 어떤 contributor도 권한을 선언하지 않음인터페이스 확인, 그다음 클래스 위치 확인
descriptor의 InvalidArgumentExceptionDescriptor 생성자 검증메시지에 표시된 잘못된 값을 수정
검증·활성화·설치 중 DescriptorValidationExceptionContribution 자체가 잘못됐거나 다른 contribution과 모순됨보고된 descriptor 경로를 모두 고친 뒤 nexia-apps:validate-app-descriptors 재실행
일부 프로세스에서만 contribution이 나타남오래 도는 프로세스의 낡은 오토로더클래스 추가 후 워커와 Vite 재시작

App에 적용

Manifest가 반환하는 디렉터리에 Contribution 클래스를 두고 인터페이스를 명시적으로 구현하세요. Trait만 사용하면 등록되지 않습니다.

관련 문서

원본 위치: docs/developers/content/ko/app-sdk/contribution-contracts.md