コンテンツにスキップ
BlackOps1.xは試験的なバージョンです。Production Readyは2.xを予定しています。Releases
BlackOps
Esc
navigateopen⌘Jpreview
このページの内容

Authoring

Typed Self-handled Operation、Value、Outcome、業務拒否の標準的な書き方を説明する。

BlackOpsの標準Authoringは、Applicationが実行したい一つの意図であるOperation自身がNative Typed handle()を持つTyped Self-handled形式です。Value、Outcome、Optional ContextをPHP Signatureで宣言し、BuildがMetadataとHandler登録を生成します。

標準形

use BlackOps\Core\Attribute\OperationType;
use BlackOps\Core\Operation;

#[OperationType('order.place')]
final readonly class PlaceOrder implements Operation
{
    public function __construct(
        private OrderRepository $orders,
    ) {}

    public function handle(PlaceOrderValue $value): OrderPlaced
    {
        $order = $this->orders->place($value->customerId, $value->productCode, $value->quantity);

        return new OrderPlaced($order->id());
    }
}
final readonly class PlaceOrderValue implements OperationValue
{
    public function __construct(
        public string $customerId,
        public string $productCode,
        public int $quantity,
    ) {}
}

final readonly class OrderPlaced implements Outcome
{
    public function __construct(public string $orderId) {}
}

第一引数は具象ClassでOperationValueを実装し、Return Classは具象ClassでOutcomeを実装します。ValueとOutcomeはSignatureから推論されるため、標準形では#[Accepts]#[Returns]OperationHandler、Generic DocBlockを追加しません。

OperationはContainerへAutowireされます。Repository Interface等のConstructor DependencyはApplicationのService ProviderからBindingします。OperationをOperation Providerへ手動列挙する必要はありません。

Contextが必要なOperation

Operation IDやDeferred Attemptが必要な場合だけ、第二引数へExecutionContextを指定します。

use BlackOps\Core\ExecutionContext;

public function handle(GenerateReportValue $value, ExecutionContext $context): ReportGenerated
{
    $attempt = $context->attempt();

    return new ReportGenerated($value->reportName, $context->operationId()->toString());
}

InlineではAttemptがnull、Deferred Workerでは現在のAttemptが入ります。Contextの詳細はExecution Contextを参照してください。

値のない成功

処理結果のValueが不要な場合はvoidを返します。

public function handle(RebuildIndexValue $value): void
{
    $this->indexes->rebuild($value->name);
}

Frameworkは成功時にEmptyOutcomeへ正規化します。

Credentialを一度だけ返す

LoginやToken Rotationのように、成功値をHTTP Clientへ返す一方でJournalやOutcome Storeへ残したくない場合はEphemeralOutcomeを使います。Ephemeralは「そのHTTP Response中だけ有効な投影」を意味し、Lifecycleそのものを隠す機能ではありません。

<?php

declare(strict_types=1);

namespace App\Feature\Identity\IssueCredential;

use BlackOps\Core\Attribute\OperationType;
use BlackOps\Core\Attribute\Sensitive;
use BlackOps\Core\EphemeralOutcome;
use BlackOps\Core\Operation;
use BlackOps\Core\OperationValue;
use BlackOps\Http\Attribute\FromBody;
use BlackOps\Http\Attribute\Route;

final readonly class IssueCredentialValue implements OperationValue
{
    public function __construct(
        #[FromBody]
        public string $email,
        #[FromBody]
        #[Sensitive]
        public string $password,
    ) {}
}

final readonly class CredentialIssued implements EphemeralOutcome
{
    public function __construct(
        #[Sensitive]
        public string $token,
        public string $expiresAt,
    ) {}
}

#[OperationType('identity.credential.issue')]
#[Route('POST', '/credentials')]
final readonly class IssueCredential implements Operation
{
    public function __construct(private CredentialService $credentials) {}

    public function handle(IssueCredentialValue $value): CredentialIssued
    {
        return $this->credentials->issue($value->email, $value->password);
    }
}

このOperationにはHTTP Routeが必要です。AttributeなしのExecution StrategyはInlineへ解決されます。Deferred、Console、Routeなし、Inline以外のStrategyはBuild Errorになります。Credential名を持つOutcome Propertyには#[Sensitive]を付け、CredentialをNested DTOへ隠さずRoot Propertyとして宣言してください。

HTTPはCredentialIssuedをJSON 200で一度返します。PropertyがないEphemeral Outcomeは{}を返します。Canonical JournalにはReceivedを空Data、CompletedをEmptyOutcomeとして記録し、Outcome StoreへRowを作りません。Status APIは認可後もUnavailableとなり、Generated Frontend Objectには.fetch().toRequest().url()だけを生成して.status().wait()を公開しません。

PHPからInline Dispatcherを直接呼ぶ場合は実Outcomeを受け取れますが、FrameworkのJournal、Observer、Status、Console、Deferred経路から復元できません。認証・認可、Cookie、CSRF、暗号化、Browser Storage、Token Rotationは引き続きApplicationが設計します。

予期された業務拒否

ValidationやBusiness Ruleによる予期された拒否だけをOperationRejectedExceptionで通知します。

use BlackOps\Core\Exception\OperationRejectedException;

if (!$this->inventory->isAvailable($value->items)) {
    throw OperationRejectedException::conflict('inventory_unavailable');
}

利用できるCategory FactoryはvalidationunauthorizedforbiddennotFoundconflictbusinessRuleです。Codeは安定した識別子とし、Credentialや自由文Payloadを含めません。

その他のThrowableはRejectedへ変換されません。一時障害はRetryable Exception、BugやInfrastructure FailureはFailure Policyへ渡されます。

Separate Handler

Decoratorや複数実装の切替等で責務を分ける場合は、#[HandledBy]OperationHandlerを使うCompatibility形を選べます。新しい単純なUse CaseではTyped Self-handledを優先してください。

Sourceを追加したらphp blackops build:compileでSignatureとMetadataを検証します。Generatorを利用する場合はGeneratorsを参照してください。