これはBEAR.Sundayの全てのマニュアルページを一つにまとめたページです。
概要
真の航海とは、新しい風景を探すことではなく、新しい目を持つことである。
—マルセル・プルースト『囚われの女』(À la recherche du temps perdu 第5巻)1923年
DoingからBeingへ
まず、これを見てください
// 従来のユーザー削除方法
$user = User::find($id);
$user->delete();
// 異なるユーザー削除方法
$activeUser = User::find($id);
$deletedUser = new DeletedUser($activeUser);
DeletedUserって何?と思うかもしれません。 これまで考えてもみなかった方法で、プログラミングを考えてみましょう。
『何をするか』から『何であるか』へ
従来のプログラミングはDOING(何をするか)に着目します:
$user->validate();
$user->save();
$user->notify();
Be FrameworkはBEING(何であるか)に着目します:
$userInput = new UserInput($name, $email);
$validatedUser = new ValidatedUser($userInput);
$savedUser = new SavedUser($validatedUser);
前者はオブジェクトに対して「何をしろ」と指示します。 後者はオブジェクトが「どのような状態になるか」を表現します。
なぜこれが重要なのか
DOINGに着目すると:
- 実行前に「このアクションは可能か?」を毎回チェックする必要があります
- 様々なエラーケースに対処しなければなりません
- 不正な状態を防ぐための処理が常に必要です
BEINGに着目すると:
- 不正な状態のオブジェクトは最初から存在しません
- オブジェクトが存在すること自体が「正しい状態」の証明になります
- その時にできることだけに集中できます。できないことはそもそも実行できません
違いは型そのものにあります:
// 従来型:汎用的な型
function processUser(User $user) { }
// Be Framework:特定の存在状態
function processUser(ValidatedUser $user) { }
function saveUser(SavedUser $user) { }
function archiveUser(DeletedUser $user) { }
各型はただのデータではなく、オブジェクトの特定の状態を表しています。時間的な変化が型で表されるので、その時点で可能な操作だけが行えます。たとえば、削除済みのオブジェクトにさらに削除を命じることはできません。
なぜ「コントローラー」ではないのか?
従来のMVCフレームワークでは、コントローラーがアプリケーション全体の流れを制御します。しかし、システムが複雑になるにつれて、この「全てを制御しようとするアプローチ」は困難になります。
コントローラーは全てのモデルやコンポーネントに無制限にアクセスできる「全能の自由」を持っています。しかし制約がないということは、システム内のあらゆる手続きの整合性を自分で保つという「無限の責任」を負うことを意味します。
Be Frameworkは異なるアプローチを採ります。操作が目的のデータをつくり出すのではなく、種子のような単純な入力オブジェクトが他のオブジェクトと出会い、自然に成長し、最終オブジェクトへと自ら変容していきます。
Commander (司令官) から Gardener (庭師) へ:
- 司令官は部下(オブジェクト)に「動け」と命令します。しかし、システムが複雑になるほど、すべてを命令で制御し続けるのは困難です。
- 庭師は、植物に命令しません。ただ、水や光という環境を整えるだけです。
植物は他者を変えようとせず、環境を受け入れて自らを変容させ、あるべき姿に成ります。Be Frameworkも同じです。入力を与えると、自らが最終オブジェクトになるような環境を整えます。制御を手放し、自律的な変容に委ねる。これがBe Frameworkのコアコンセプトです。
このマニュアルで学べること
以下の新しいプログラミング手法を身につけることができます:
- 「何をするか」ではなく「何であるか」を設計する
- 不正な状態をチェックするのではなく、最初から作れないようにする
- オブジェクトを無理に変更するのではなく、自然な変容(自己変容)を表現する
- エラーを防ぐのではなく、正しい状態を信頼する
なぜ「DeletedUser」なのか?
冒頭の問いに戻りましょう。new DeletedUser($activeUser) は操作ではなく、変容です。ユーザーが「削除される」のではなく、$activeUserからDeletedUserという新しい存在が生まれるのです。型そのものが削除済みであることを証明しています。$statusフラグを確認する必要も、削除済みのユーザーに誤ってメソッドを呼ぶ危険もありません。これがBe Frameworkの本質です:状態遷移を既存オブジェクトへの操作ではなく、新しい型として表現する。
さあ、始めましょう
まず動かしてみたい方は? Getting StartedでHello Worldを体験するか、チュートリアルで実践的な例に挑戦できます。
概念から理解したい方は? 入力クラスに進んで、基礎からステップバイステップで学びましょう →
入力クラス
「私たちは自分で選択できない条件から始まり、そこから自分の存在を築く」
—ハイデガーの被投性(Geworfenheit)概念より(『存在と時間』1927年)
出発点
入力クラスは、Be Frameworkにおけるすべての変容の出発点です。
入力クラスにはオブジェクト自身が持つ要素だけが含まれ、外部依存がありません。これがオブジェクトの本質的な属性です。オブジェクトの内側にあるものなので、これを内在(Immanence)と呼びます。
基本構造
#[Be(ValidatedUser::class)] // 変容の運命
final readonly class UserInput
{
public function __construct(
public string $name, // 内在
public string $email // 内在
) {}
}
主要な特徴
純粋なアイデンティティ: 入力クラスはオブジェクトが何であるかだけを含みます。外部依存や複雑なロジックはありません。
ユースケースの起点: すべてのユースケースは固有の入力クラスを持ちます。
変容先(オブジェクトの運命): #[Be()]属性は、この入力が何になるかを宣言します。
読み取り専用プロパティ: すべてのプロパティは readonly です。入力クラスの値は変更されません。
例
単純なデータ入力
#[Be(OrderCalculation::class)]
final readonly class OrderInput
{
public function __construct(
public array $items, // 内在
public string $currency // 内在
) {}
}
複雑な構造化入力
#[Be(PaymentProcessing::class)]
final readonly class PaymentInput
{
public function __construct(
public Money $amount, // 内在
public CreditCard $card, // 内在
public Address $billing // 内在
) {}
}
入力クラスが外の世界と出会い、変容を始めます。その過程を存在クラスで見ていきます ➡️
存在クラス
「道常無為而無不為」
—道は常に無為にして、而も為さざることなし(老子『道徳経』第三十七章 紀元前6世紀)
内在と超越
入力クラスが「始まり」なら、存在クラスは「変容した存在」を表現します。
入力クラスのpublicプロパティは、存在クラスのコンストラクタに引き継がれます。この引き継がれた値を内在(Immanence)と呼びます。内在はオブジェクトのアイデンティティであり、変容を経ても保たれます。
コンストラクタには外部から超越(Transcendence)もインジェクトされます。超越は内在と出会い、新しい内在へと変容させます。
基本構造
final readonly class ValidatedUser
{
public string $displayName;
public bool $isValid;
public function __construct(
#[Input] string $name, // 内在
#[Input] string $email, // 内在
#[Inject] NameFormatter $formatter, // 超越
#[Inject] EmailValidator $validator // 超越
) {
$this->displayName = $formatter->format($name); // 新しい内在
$this->isValid = $validator->validate($email); // 新しい内在
}
}
#[Input]パラメータには、前のクラスのpublicプロパティが名前の一致により自動的に渡されます。UserInputのpublic string $nameはValidatedUserの#[Input] string $nameに対応します。#[Inject]パラメータにはDIコンテナから外部の依存が注入されます。この自動マッチングの詳細なルールは5章 変容で説明します。
時間的存在としてのオブジェクト
Be Frameworkでは、オブジェクトを静的なデータ構造ではなく、特定の時間の中でのみ存在する時間的な存在として捉えます。
誕生
コンストラクタは、オブジェクトが生まれる場所です。すべての存在クラスは同じ変容の流れに従います:
内在 (#[Input]) + 超越 (#[Inject]) → 新しい内在
内在が超越と出会い、変容のロジックが働き、プロパティの代入によって新しい内在が生まれます。生まれた瞬間、そのオブジェクトのアイデンティティと状態は確定し、不変(Immutable)となります。
生
オブジェクトは public readonly プロパティとして、その「あるべき姿」を世界に晒します。フレームワークがこのプロパティを読み取り、次のクラスの #[Input] として引き渡します。その後、オブジェクトは消滅し、次の存在に道を譲ります。
なりたい自分になる
すべての変容は、最終的な「なりたい自分、なるべき自分(Final Object)」になるための旅です。
出会った超越は、新しい内在に影響を与えて消滅します。幼少期の友人のように、オブジェクトを形作りその一部となりますが、その時だけの存在として、やがて消えていきます。
変容の例
final readonly class OrderCalculation
{
public Money $subtotal;
public Money $tax;
public Money $total;
public function __construct(
#[Input] array $items, // 内在
#[Input] string $currency, // 内在
#[Inject] PriceCalculator $calculator, // 超越
#[Inject] TaxService $taxService // 超越
) {
$this->subtotal = $calculator->calculateSubtotal($items, $currency);
$this->tax = $taxService->calculateTax($this->subtotal);
$this->total = $this->subtotal->add($this->tax); // 新しい内在
}
}
OrderCalculationは計算された注文になりたい。ValidatedUserは検証済みユーザーになりたい。各クラスがコンストラクタで「なりたい自分」になるのです。Be Frameworkでは、動作(DOING)ではなく、存在(BEING)を中心に考えます。
変容についての考察
内在だけでは変われません。データがどれほど豊かでも、それ自身の力で新しい存在にはなれないのです。変容には必ず超越—自分の外にある力—との出会いが必要です。そして出会った超越は内在を変え、自らは消えていきます。この「出会い、変わり、消える」というパターンは、コードに限った話ではありません。小麦粉はイーストと熱に出会ってパンになります。ぶどうは酵母と時間に出会ってワインになります。種は土と水と光に出会って花になります。あらゆるドメインの変容がこのパターンに従います。
自然な流れ
存在クラスは何かを「する」わけではありません。自分が持つもの(内在)と、外部から与えられる力(超越)が出会うことで、自然にあるべき姿へと変容します。この流れを指揮するものはいません。各オブジェクトはただ次の存在に渡されるだけです。
変容の行き着く先、最終オブジェクトへ ➡️
最終オブジェクト
「あなたは私ではない。どうして私が魚の気持ちを知らないと分かるのか?」
—「あなたは魚ではない。どうして魚の気持ちが分かるのか」と問われた時に荘子が返した言葉 (『荘子』紀元前4世紀)
終着点
ユーザーにとって見えるのは、入力と最終オブジェクトだけです。入力クラスで始まった旅が、最終オブジェクトとして届く—これが変容の到達点です。
基本構造
final readonly class SuccessfulOrder
{
public string $orderId;
public string $confirmationCode;
public DateTimeImmutable $timestamp;
public string $message;
public BeenConfirmed $been; // 完了の証跡
public function __construct(
#[Input] Money $total, // 内在
#[Input] CreditCard $card, // 内在
#[Inject] OrderIdGenerator $generator, // 超越
#[Inject] Receipt $receipt // 超越
) {
$this->orderId = $generator->generate();
$this->confirmationCode = $receipt->generate($total);
$this->timestamp = new DateTimeImmutable();
$this->message = "注文確認: {$this->orderId}";
$this->been = new BeenConfirmed(
actor: $card->getHolderName(),
timestamp: $this->timestamp,
evidence: [
'total' => $total->getAmount(),
'payment_method' => $card->getType(),
'confirmation' => $this->confirmationCode
]
);
}
}
$beenプロパティはアプリケーションが定義する、ドメイン固有の型を持ちます。SuccessfulOrderにはBeenConfirmed、FailedOrderにはBeenRejected、DeletedUserならBeenDeletedというように命名します。何を「完了の証跡」とするかはドメインによって異なり、必要な証跡に応じてクラスを設計します。
入力クラスとは対照的に、最終オブジェクトはドメインの豊かさを完全に表現した存在です。内在が超越と出会い、変容を経て、これ以上変わる必要のない完全な状態に達しています。
時間的存在の完全性
Be Frameworkでは、オブジェクトの時間的存在を二つの軸で捉えます:
#[Be]: なりたい自分、向かう先(未来への方向性)$been: 完了した自分(過去完了の証跡)
$orderIdや$confirmationCodeはビジネス上の本質的な値です。一方$beenは、いつ・誰が・何を根拠に完了したかという証跡を記録します。
内側からの完全性
従来のプログラミングでは、オブジェクトが正しく処理されたかどうかを外部のテストが判定します。しかし最終オブジェクトは、自分が何であるかを自分自身の構造として持っています。何が入力され、何が起こり、何になったか—その全てが一つの存在の中に収まり、完了の証拠になっています。
入力クラスとの対比
| 入力クラス | 最終オブジェクト |
|---|---|
| 変容の出発点 | 変容の到達点 |
| ユーザーが提供するもの | ユーザーが受け取るもの |
| シンプルな構造 | 豊かで完全な状態 |
複数の最終的運命
オブジェクトはその性質によって複数の最終形態を持つことができます。ここで使っている$becomingは変容チェーンを起動する仕組みで、次章で説明します:
$order = $becoming(new OrderInput($items, $card));
if ($order instanceof SuccessfulOrder) {
echo $order->confirmationCode;
} else {
echo $order->message; // エラーメッセージ
}
設計者の仕事
入力と最終オブジェクトの間にある変容の仕組みを設計すること—それが設計者の責務です。ユーザーが触れるのは両端だけですが、その間の存在クラスがシステムの骨格になります。
最終オブジェクトは、自分が完了したことを内側から知っています。
宣言された変容はどう動き出すのか。生成へ ➡️
生成
「存在は無へと移行し、無は存在へと移行する。この運動が生成である」
—G.W.F. ヘーゲル『大論理学』(1812年)
変容の起動
2-4章で、入力クラス、存在クラス、最終オブジェクトの構造を見てきました。#[Be()]属性は「何になるか」を宣言しますが、宣言だけでは何も起きません。その宣言を実行に移すのがBecomingです:
$finalObject = $becoming(new EmailInput($name, $email));
EmailInput → EmailValidation → UserCreation → WelcomeMessage。各クラスの#[Be()]宣言に従い、チェーン全体が自動的に実行されます。各オブジェクトは次のオブジェクトを生み出すと消滅し、この連鎖が終端まで続きます。
Becomingの取得
BecomingはDIコンテナから注入されます:
final readonly class UserRegistrationPage
{
public function __construct(
private BecomingInterface $becoming
) {}
public function __invoke(string $name, string $email): WelcomeMessage
{
return ($this->becoming)(new EmailInput($name, $email));
}
}
呼び出し側が知るのは、何を入れて何が出てくるかだけです。途中の変容は#[Be()]宣言が決めます。
ネストした生成
存在クラスの中でBecomingを使うことで、生成の中に別の生成を含むことができます:
final readonly class OrderProcessing
{
public PaymentResult $payment;
public ShippingResult $shipping;
public function __construct(
#[Input] Order $order, // 内在
#[Inject] Becoming $becoming // 超越
) {
$this->payment = $becoming(new PaymentInput($order->getPayment()));
$this->shipping = $becoming(new ShippingInput($order->getAddress()));
}
}
Becomingは超越として注入されるため、存在クラスの内部からも変容チェーンを起動できます。分岐した結果を最後に1つのオブジェクトに収束させるダイアモンド型の構成も可能です。
道は1つではありません。変容で様々な道を学びます ➡️
変容
「空間と時間は独立に定義できない」
—アルベルト・アインシュタイン『一般相対性理論の基礎』(1916年)
時間とドメインは分割できない
アインシュタインが時間と空間の不可分性を発見したように、Be Frameworkでは時間とドメインは分割できない一つの実体です。承認プロセスには承認の時間が、決済には決済の時間があり、それぞれのドメインロジックが持つ固有の時間軸に沿って変容が自然に現れます。
不可逆的時間の流れ
オブジェクトの変容は時間の矢に沿った一方向の流れです。過去に戻ることも、同じ瞬間に留まることもできません:
// 時間 T0: 入力の誕生
#[Be(EmailValidation::class)]
final readonly class EmailInput { /* ... */ }
// 時間 T1: 第一変容(T0は既に過去)
#[Be(UserCreation::class)]
final readonly class EmailValidation { /* ... */ }
// 時間 T2: 第二変容(T1は記憶となる)
#[Be(WelcomeMessage::class)]
final readonly class UserCreation { /* ... */ }
// 時間 T3: 最終存在(すべての過去を内包)
final readonly class WelcomeMessage { /* ... */ }
各瞬間は二度と戻らず、新しい存在は前の形態をその内部に記憶として保持します。川が流れるように、時間は一方向にのみ流れます。
運命の自己決定
現実の生物と同様に、オブジェクトは内在と超越の相互作用によって、自身の運命を決定します。これは予め決められたルートを辿るのではなく、その瞬間の状況に応じた自然な変容です:
#[Be([ApprovalNotification::class, RejectionNotification::class])]
final readonly class ApplicationReview
{
public Approved|Rejected $being;
public function __construct(
#[Input] string $email, // 内在
#[Input] array $documents, // 内在
#[Inject] ReviewService $reviewer // 超越
) {
$result = $reviewer->evaluate($documents);
// 運命は今この瞬間に決まる
$this->being = $result->isApproved()
? new Approved($email, $result->getScore())
: new Rejected($email, $result->getReasons());
}
}
ApprovedとRejectedはReasonオブジェクトです。TutorialのEmergencyやObservationと同じ役割です。理由(Reason)が運命を決めます。判定結果とその根拠を持ち、$beingに代入されると、その型が次のクラスを決定します。コンストラクタ内で完結する判定ロジックはReason層に置きます。一方、生成後に呼び出される振る舞い—TutorialのassignER()のような遅延実行メソッド—はFinalクラスに持たせます。
型による継続
#[Be()]で指定された候補クラスのうち、現在のオブジェクトのpublicプロパティで#[Input]コンストラクタ引数を満たせるクラスが自動的に選択されます:
// ApplicationReviewの$beingがApproved型なので
// #[Input] Approvedがマッチするこのクラスが選択される
final readonly class ApprovalNotification
{
public function __construct(
#[Input] Approved $approval,
#[Inject] Mailer $mailer
) {
$mailer->send($approval->email, 'Approved! Score: ' . $approval->score);
}
}
$beingはこの自己決定パターンでよく使われるプロパティ名のコンベンションですが、フレームワークが要求する名前ではありません。どのpublicプロパティでもマッチングの対象になります。
自己組織化パイプライン
Unixパイプが単純なコマンドを組み合わせて強力なシステムを作るように、Be Frameworkは型付きオブジェクトを組み合わせて自然な変容の流れを作ります。
# Unix: テキストが流れる外部制御のパイプライン
cat access.log | grep "404" | awk '{print $7}' | sort | uniq -c
Unixではshellがパイプを制御しますが、Be Frameworkではオブジェクト自身が#[Be()]で運命を宣言します。コントローラーやオーケストレーターのような外部の制御は存在しません。
ヘラクレイトスは「流れているのが川だ」と言いました。川が流れるのではなく、流れそのものが川であるように、Be Frameworkのドメインは、終端に至るまで流れ続ける時間的存在です。
変数名が制約と契約をもつ意味変数へ ➡️
意味変数
「存在するものは必然的に存在し、存在しないものは必然的に存在しない」
—スピノザ『エチカ』第1部定理29(1677年)
意味と制約
$emailは単なる文字列ではありません。意味変数は情報の識別子であり、意味を表し、制約を持つ完全な情報モデルです:
final class Email
{
#[Validate]
public function validate(string $email): void
{
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new InvalidEmailException();
}
}
}
// $emailという名前を持つ任意のコンストラクタ引数に自動適用される
public function __construct(string $email) {}
一度定義すれば、$emailという名前のすべてのコンストラクタ引数に自動的に適用されます。$emailの値が正しいのは偶然ではなく、必然です。正しくなれないものは存在できません。
属性による制約の拡張
同じ名前に属性を加えることで、存在条件をより精密にできます。#[Validate]属性が付いたメソッドでは、メソッド引数に属性(例: #[Teen])が指定されていると、コンストラクタ側に同じ属性があるときだけ実行されます:
// $ageの基本制約(0-150歳)
final readonly class Age
{
#[Validate]
public function validate(int $age): void
{
if ($age < 0 || $age > 150) {
throw new InvalidAgeException();
}
}
// #[Teen]属性がある場合のみ追加実行される
#[Validate]
public function validateTeen(#[Teen] int $age): void
{
if ($age < 13 || $age > 19) {
throw new InvalidTeenAgeException();
}
}
}
// 基本のAge検証のみ適用
public function __construct(int $age) {}
// Age検証 + Teen検証の両方が適用
public function __construct(#[Teen] int $age) {}
同じ$ageでも、属性によって異なる存在条件が適用されます。
名前が関係を持つ
意味変数は単独の制約だけでなく、変数間の関係も制約として持ちます。#[Validate]属性が付いたメソッドの引数名がコンストラクタの引数名と部分一致すると、対応する値が自動的に渡されます:
// フォーマット検証 — メールアドレスの一致確認
final readonly class EmailConfirmation
{
#[Validate]
public function validate(string $email, string $confirmEmail): void
{
if ($email !== $confirmEmail) {
throw new EmailMismatchException();
}
}
}
// 順序比較 — 開始日は終了日より前
final readonly class DateRange
{
#[Validate]
public function validate(string $startDate, string $endDate): void
{
if ($startDate > $endDate) {
throw new InvalidDateRangeException();
}
}
}
// 外部サービス参照 — 郵便番号と都道府県の整合性
final readonly class ZipPrefecture
{
public function __construct(
private ZipResolver $resolver // DIで注入
) {}
#[Validate]
public function validate(string $zipCode, string $prefecture): void
{
if (!$this->resolver->matches($zipCode, $prefecture)) {
throw new ZipPrefectureMismatchException();
}
}
}
一度定義すれば、引数名がマッチする任意のコンストラクタに自動適用されます:
// EmailConfirmation + DateRange が自動適用
public function __construct(
string $email,
string $confirmEmail,
string $startDate,
string $endDate,
) {}
// ZipPrefecture が自動適用
public function __construct(
string $zipCode,
string $prefecture,
) {}
名前に宿る制約
名前の力はフォーマット検証にとどまりません:
public function __construct(
public string $email, // フォーマット制約
public float $bodyTemperature, // 値の範囲制約
public string $inStockItemId, // ビジネスルール制約
) {}
フォーマットから値の範囲、さらにビジネスルールまで——名前が存在の条件を定義します。
名前に宿る制約が守られないとき、存在は失敗します。その扱い方は意味例外で学びます。
存在する理由のない存在はありません。存在理由層へ ➡️
存在理由層
「すべてのものには、それが存在するための理由がある」
—ライプニッツ『充足理由律』(1714年)
存在の理由
ExpressDeliveryが速達配送として成り立つのは、速達配送の能力を持っているからです。StandardDeliveryが通常配送として成り立つのは、通常配送の能力を持っているからです。この「なぜその存在でいられるのか」の根拠が、raison d’être(レーゾンデートル:存在理由)です。
存在理由層は、このraison d’êtreを一つのオブジェクトとして表現する設計パターンです。
final readonly class ExpressDelivery
{
public Fee $fee;
public function __construct(
#[Input] OrderData $order, // 内在
#[Inject] ExpressShipping $reason // 存在理由
) {
$this->fee = $reason->calculateFee($order->weight);
}
}
ExpressShippingがExpressDeliveryのraison d’êtreです。速達配送に必要な道具一式をまとめて提供します。
$beingとしての存在理由
存在理由オブジェクトは$beingプロパティとしても使えます。このとき、そのオブジェクトの型が変容先の判別根拠になると同時に、その存在様式に固有のメソッドも提供します。
final readonly class ExpressDelivery
{
public Fee $fee;
public function __construct(
#[Input] OrderData $order,
#[Input] ExpressShipping $being // 型が変容先を決定し、速達固有のメソッドを提供
) {
$this->fee = $being->calculateFee($order->weight);
}
}
final readonly class StandardDelivery
{
public Fee $fee;
public function __construct(
#[Input] OrderData $order,
#[Input] StandardShipping $being // 型が変容先を決定し、通常配送固有のメソッドを提供
) {
$this->fee = $being->calculateFee($order->weight);
}
}
ExpressShipping $beingという型そのものが、なぜExpressDeliveryになるのかの理由です。フレームワークはこの型を読み取り、対応する変容先を自動選択します。
どのReasonオブジェクトも#[Inject](超越の能力を提供)にも$being(運命を決定)にもなれます。違いはオブジェクト自体ではなく、使われ方にあります。ある文脈で#[Inject]として患者を評価するJTASProtocolが、別の文脈では$beingとして運命を決定することもできます。
存在理由クラスの定義
存在理由クラスは、特定の存在様式を実現するために必要なサービスをまとめたものです:
namespace App\Reason;
final readonly class ExpressShipping
{
public function __construct(
private PriorityCarrier $carrier,
private RealTimeTracker $tracker,
) {}
public function calculateFee(Weight $weight): Fee // 速達料金
{
return $this->carrier->expressFee($weight);
}
public function guaranteeDeliveryBy(Address $address): \DateTimeImmutable // 配達日保証
{
return $this->carrier->guaranteedDate($address);
}
public function realTimeTrack(TrackingId $id): TrackingStatus // リアルタイム追跡
{
return $this->tracker->realTimeStatus($id);
}
}
final readonly class StandardShipping
{
public function __construct(
private RegularCarrier $carrier,
private BatchTracker $tracker,
) {}
public function calculateFee(Weight $weight): Fee // 通常料金
{
return $this->carrier->standardFee($weight);
}
public function estimateDeliveryWindow(Address $address): DateRange // 配達期間の見積もり
{
return $this->carrier->estimateWindow($address);
}
}
個別注入との違い
存在理由層は#[Inject]を使います。では複数の#[Inject]をバラバラに使う場合と何が違うのでしょうか。
個別の注入:
public function __construct(
#[Input] OrderData $order,
#[Inject] PriorityCarrier $carrier,
#[Inject] RealTimeTracker $tracker,
#[Inject] InsuranceService $insurance,
#[Inject] DeliveryScheduler $scheduler
) {
// バラバラの道具を個別に使用
}
存在理由層:
public function __construct(
#[Input] OrderData $order,
#[Inject] ExpressShipping $reason // 関連道具がまとまった存在理由
) {
$this->fee = $reason->calculateFee($order->weight);
}
「ExpressDeliveryになるには何が必要か?」という問いに、存在理由オブジェクト一つが答えます。オブジェクト自身は「何になるか」を宣言し、存在理由は「どうやってその状態になるか」を実現します。
Potentialを返すReason
ここまでのReasonはFeeのような即時の値を返していました。しかし注文処理を考えてみましょう。在庫確保・決済・配送手配のすべてが成功してからコミットする必要があります。決済が失敗したのに在庫だけ確保されたままでは困ります。
このような一括実現が必要な場面で、Reasonは値の代わりにPotentialを返します。Potentialは遅延操作を保持するオブジェクトで、後からbe()で実現されます。このパターンは複数の外部操作をアトミックにコミットする必要があるときだけ使います。
Potential: 準備済み・未コミット
Reasonのメソッドは外部操作を準備し、Potentialを返します:
final class PaymentGateway
{
public function authorize(string $cardNumber, int $amount): PaymentCapture
{
$authCode = $this->api->authorize($cardNumber, $amount);
return new PaymentCapture(
$authCode,
$amount,
fn () => $this->api->capture($authCode, $amount),
);
}
}
PaymentCaptureはPotentialです。認証コードとキャプチャの遅延操作を保持しています。決済は認証済みですが、まだ確定していません。be()で確定します:
$capture = $gateway->authorize($cardNumber, $amount);
$capture->authorizationCode; // 即座に利用可能
$capture->be(); // キャプチャを確定
Moment: Potentialを保持する
ReasonからPotentialを受け取って保持するクラスをMoment(ヘーゲルの契機—全体の中でのみ意味を持つ不可欠な側面)と呼びます。Momentはフレームワークが提供するMomentInterfaceを実装します:
interface MomentInterface
{
public function be(): void;
}
final readonly class PaymentCompleted implements MomentInterface
{
public PaymentCapture $capture;
public function __construct(
#[Input] public string $cardNumber,
#[Input] public int $amount,
#[Inject] PaymentGateway $gateway,
) {
$this->capture = $gateway->authorize($cardNumber, $amount);
}
public function be(): void
{
$this->capture->be();
}
}
収束: FinalがMomentを実現する
複数のMomentがすべて揃う必要があるとき、Final Objectはそれらを受け取り、各Momentのbe()を呼びます。これは外部からの命令ではなく、自己完成です:
final readonly class OrderConfirmed
{
public string $orderId;
public string $status;
public function __construct(
public InventoryReserved $inventory,
public PaymentCompleted $payment,
public ShippingArranged $shipping,
) {
$this->inventory->be();
$this->payment->be();
$this->shipping->be();
$this->orderId = 'ORD-' . date('Ymd') . '-' . bin2hex(random_bytes(4));
$this->status = 'confirmed';
}
}
いずれかのMomentが生成できなければ(Reasonが失敗したため)、Final Objectは構築されません。すべてのMomentが存在すれば、be()がすべての遅延操作をコミットします。手動のロールバックフラグもネストされたtry-catchも不要です。
このパターンを使う場面
複数の外部操作をアトミックに成功させる必要があるときに、Potentialを返すReasonを使います。Reasonが即時の値を返す単純なケースでは不要です。
存在できなかった、という結果もまた扱う必要があります。検証とエラーハンドリングでその扱い方を学びます ➡️
意味例外
「過ちて改めざる、これを過ちという」
—孔子『論語』(紀元前551-479年)
意味のある失敗
汎用例外は何が起きたかを伝えます:
catch (Exception $e) {
echo $e->getMessage(); // "検証に失敗しました"
}
それに対して意味例外はなぜ存在できないかを伝えます:
catch (SemanticVariableException $e) {
foreach ($e->getErrors()->exceptions as $exception) {
echo get_class($exception) . ": " . $exception->getMessage();
// EmptyNameException: 名前は空にできません
// InvalidEmailException: メール形式が無効です
}
}
ドメイン例外クラス
すべての例外はPHPの\DomainExceptionを継承します。ドメイン層では技術的例外(RuntimeException、InvalidArgumentException等)を使わず、常にドメイン例外を使います。失敗は常にドメインの意味を持つ失敗として表現されます:
final readonly class EmptyNameException extends \DomainException {}
final readonly class InvalidEmailException extends \DomainException
{
public function __construct(public string $invalidEmail)
{
parent::__construct("メール形式が無効です: {$invalidEmail}");
}
}
// 年齢関連の存在失敗
abstract class AgeException extends \DomainException {}
final readonly class NegativeAgeException extends AgeException {}
final readonly class AgeTooHighException extends AgeException {}
ドメイン例外はメッセージだけでなく構造化データを持ちます。$invalidEmailプロパティから、プログラムは無効なメールアドレスの値にアクセスできます——表示、APIレスポンス、ログなど、さまざまな用途に:
catch (InvalidEmailException $e) {
$logData = [
'invalid_email' => $e->invalidEmail, // プログラムからアクセス可能
'user_ip' => $request->getClientIp(),
'timestamp' => now()
];
Logger::warning('Invalid email attempt', $logData);
}
多言語メッセージ
#[Message]属性で、例外はユーザーの言語で話します:
#[Message([
'en' => 'Name cannot be empty.',
'ja' => '名前は空にできません。',
'es' => 'El nombre no puede estar vacío.'
])]
final readonly class EmptyNameException extends \DomainException {}
#[Message([
'en' => 'Age must be at least {min} years.',
'ja' => '年齢は最低{min}歳でなければなりません。'
])]
final readonly class AgeTooYoungException extends \DomainException
{
public function __construct(public int $min = 13) {}
}
エラー収集
フレームワークは最初のエラーで止まらず、すべての検証エラーを収集します:
try {
$user = $becoming(new UserInput('', 'invalid-email', 10));
} catch (SemanticVariableException $e) {
// 3つのエラーが同時に収集される:
// - EmptyNameException
// - InvalidEmailException
// - AgeTooYoungException
$messages = $e->getErrors()->getMessages('ja');
// ["名前は空にできません", "メール形式が無効です", "年齢は最低13歳でなければなりません"]
}
最初のエラーで即座に失敗するのではなく、すべての問題を一度に把握できます。
エラーも存在の1つ
エラー状態も変容の有効な結果として扱えます:
#[Be([ValidUser::class, InvalidUser::class])]
final readonly class UserValidation
{
public ValidUser|InvalidUser $being;
public function __construct(#[Input] string $data)
{
try {
$this->being = new ValidUser($data);
} catch (ValidationException $e) {
$this->being = new InvalidUser($e->getErrors());
}
}
}
例外で実行を止めるのではなく、エラーを型として表現する。失敗も成功と同じく、変容の正当な結果です。
フレームワークの全体像はリファレンスへ ➡️
意味的ログ
「記録されるものは記憶となり、記憶されるものは真実となる」
—オーウェル『1984年』の概念より(1949年)
概要
従来のログ:
[INFO] User registered: alice@example.com
[INFO] Verification passed
[INFO] Insert into users table
意味的ログ:
{
"open": { "from": "UnverifiedEmail", "to": "RegisteredUser" },
"events": [
{ "type": "email_format_asserted", "context": { "email": "alice@example.com" } },
{ "type": "user_inserted", "context": { "userId": 42, "email": "alice@example.com" } }
],
"close": { "properties": { "userId": 42, "value": "alice@example.com" } }
}
従来のログは行単位のテキストが時系列に並ぶだけで、どの行が同じ操作に属するかは読み手の推測に委ねられます。
意味的ログでは、ひとつの変容がひとつのJSONに収まります。変容元と変容先、途中の出来事、最終プロパティ — 「何が何になり、なぜそうなったか」が型付きの構造化データとして記録され、JSONスキーマで検証できます。
Be Frameworkには二つの意味的記録の仕組みがあります。
$been— Finalオブジェクトが自分の来歴を保持する証明(proof)SemanticLoggerInterface— 階層的な操作を記録するログ(log)
| log | $been |
|
|---|---|---|
| 性質 | descriptive(記述) | constitutive(構成) |
| 視点 | 第三者(観測者) | 一人称 |
| 問い | 何が起きたか | なぜ今の私なのか |
| 文法 | doing | being |
| 役割 | 記録 | 証明 |
$been — 存在証明
FinalオブジェクトのコンストラクタでBeenをインジェクトし、with()で出来事を記録していくと、そのオブジェクトがなぜ今の状態にあるかの証明になります。
final class RegisteredUser
{
public readonly int $userId;
public readonly Been $been;
public function __construct(
#[Input] string $value,
#[Inject] EmailVerifier $verifier,
#[Inject] UserRepository $users,
#[Inject] Been $been,
) {
if (! $verifier->check($value)) {
throw new UnbecomingException('email format failed');
}
$this->userId = $users->insert(['email' => $value]);
$this->been = $been
->with(new EmailFormatAssertedContext(
email: $value,
))
->with(new UserInsertedContext(
userId: $this->userId,
email: $value,
));
}
}
Beenは#[Inject]でDIコンテナから受け取ります。受け取ったBeenにはフレームワークが変容開始時に記録した変容元・変容先の情報がすでに含まれています。開発者はwith()で、Finalオブジェクトの内部でしか知り得ない出来事 — メールを検証した、ユーザーを挿入した — を追記します。
イベントコンテキスト
Beenに渡すイベントはAbstractContextのサブクラスです。
final class EmailFormatAssertedContext extends AbstractContext
{
public const string TYPE = 'email_format_asserted';
public const string SCHEMA_URL = 'https://myvendor.example.com/schemas/email-format-asserted.json';
public function __construct(
public readonly string $email,
) {}
}
TYPEはログ上のイベント種別、SCHEMA_URLはそのイベントのJSONスキーマを指します。コンストラクタのプロパティがそのままJSONのcontextフィールドになります。
DDDでいうドメインイベント — ビジネス上「起きたこと」を表すオブジェクトです。Finalオブジェクトが完了までに経験した事実を、アプリケーション固有のイベントコンテキストとして定義します。
SemanticLoggerInterface — 階層的な操作記録
階層的な操作記録が必要な場合は、SemanticLoggerInterfaceを直接インジェクトします。
final class RegisteredUser
{
public function __construct(
#[Input] string $value,
#[Inject] UserRepository $users,
#[Inject] SemanticLoggerInterface $logger,
) {
// 意図を宣言(open)
$id = $logger->open(new DbTransactionContext(table: 'users'));
// 途中の出来事を記録(event)
$this->userId = $users->insert(['email' => $value]);
$logger->event(new RowInsertedContext(userId: $this->userId));
// 結果を記録(close)
$logger->close(new TransactionResultContext(committed: true), $id);
}
}
open/event/closeはKoriym.SemanticLoggerの階層構造をそのまま使います。意図(intent)→ 出来事(occurrences)→ 結果(result)の三層で、ひとまとまりの操作を記録できます。
$beenはFinalオブジェクトが何であるかの証明です。SemanticLoggerInterfaceは従来のログに近い、途中経過の詳細な記録です。通常は$beenで足ります。
変容の自動記録
$beenやSemanticLoggerInterfaceとは別に、変容そのものもフレームワークがopen/closeで自動記録します。開発者がこの記録コードを書く必要はありません。
出力されるJSONの全体像です。
{
"open": {
"type": "metamorphosis_open",
"context": {
"fromClass": "MyVendor\\MyApp\\UnverifiedEmail",
"beAttribute": "#[Be(RegisteredUser::class)]",
"immanentSources": {
"value": "MyVendor\\MyApp\\UnverifiedEmail::value"
},
"transcendentSources": {
"verifier": "MyVendor\\MyApp\\EmailVerifier",
"users": "MyVendor\\MyApp\\UserRepository",
"been": "Be\\Framework\\SemanticLog\\Been"
}
}
},
"events": [
{
"type": "email_format_asserted",
"context": { "email": "alice@example.com" }
},
{
"type": "user_inserted",
"context": { "userId": 42, "email": "alice@example.com" }
}
],
"close": {
"type": "metamorphosis_close",
"context": {
"properties": { "userId": 42, "value": "alice@example.com" },
"be": { "finalClass": "MyVendor\\MyApp\\RegisteredUser" }
}
}
}
openが変容の意図(何から何へ、どの材料で)、eventsが$been->with()で記録された出来事、closeが結果(最終プロパティと変容先)です。
ログからDSLへ
従来のログは実行の記録です。コードが走った後に生まれ、デバッグに使われ、やがて消えます。
このJSONは違います。実行の記録であると同時に、変容の仕様でもあり、存在の証明でもあります。どこから来て、どう成ったのか、何であるのかの記録です。「UnverifiedEmailがRegisteredUserになる過程でemail_format_assertedとuser_insertedが起きる」— これは過去の事実の記述としても、未来の期待の宣言としても読めます。しかも型付きの構造化データなので、AIが読み書きできるDSLとしても機能します。
記録、仕様、証明、DSL。この四つが同じJSONに重なるとき、JSONスキーマによるテスト並みの厳密な検証と、ログからコードを生成しコードからログを生成する循環が可能になりえます。
技術的基盤: Koriym.SemanticLogger
フレームワークの全体像はリファレンスへ ➡️
リファレンス
「知は行の始なり、行は知の成るなり」
—王陽明『伝習録』(1518年)
公式リポジトリ
- Be Framework Core — フレームワーク本体
- Application Skeleton — プロジェクト開始用スケルトン
- コンセプト段階ドキュメント — 設計思想の変遷を辿る初期ドキュメント群
開発リファレンス
背後にある哲学
「万物は流転する」(パンタ・レイ) ——ヘラクレイトス(紀元前535-475年)
すべては存在である
Be——Being is Everything。このフレームワークは「すべては存在である」という前提のもとに作られています。
ドメインに対する問いを深めていったとき、私たちは存在の問いにたどり着きました。何が存在を可能にするのか。存在はどのように変容するのか。存在しないとはどういうことか。
2500年にわたり、東洋の思想家も西洋の哲学者も存在に対する問いを深めてきました。しかしこのフレームワークは哲学を設計原則として採用したのではありません。ドメインへの問いを深めた結果、それらの教えが証言のように感じられたのです。
2.「WHETHER?」という問い
手続き型はHOW?——どうやるか。OOPはWHAT?——それは何か。存在論的プログラミングはまず、WHETHER?——そもそも存在できるか、と問います。
#[Be(ValidatedUser::class)]
final readonly class UserInput
{
public function __construct(
public string $email,
public int $age
) {}
}
条件が満たされればValidatedUserは存在します。満たされなければ、存在しません。
4. ヘラクレイトス:万物は流転する
時間の中のオブジェクト
ヘラクレイトスは、同じ川に二度入ることはできないと観察しました。
従来のオブジェクトは、しばしば時間の外に存在します:
$user->age = 5;
$user->age = 50; // 同じオブジェクト、違う年齢
$user->delete();
$user->getName(); // 削除後に?
時間的な順序
一つの観察:ドメイン概念にはしばしば自然な時間的順序があります。
// 型でこの順序を表現できる:
UserInput → RegisteredUser → ActiveUser → DeletedUser
各段階は区別されます。DeletedUser型はActiveUserにはなれない——型システムがこの制約を反映しています。
時間 T0: EmailInput — 初期状態
↓
時間 T1: ValidatedEmail — 検証後
↓
時間 T2: RegisteredUser — 登録後
各段階は部分的な状態ではなく、完全な状態を表します。
6. 無為:強制しない
老子は書きました:
「道は常に無為にして、而も為さざるは無し」
これは、結果を強制するのではなく、自然な流れに従って作用することを示唆しています。
// 強制する
$controller->forceUserToValidate();
$controller->forceUserToSave();
// 可能にする
#[Be(ValidatedUser::class)]
#[Be(SavedUser::class)]
$user = $becoming(new UserInput($data));
後者のアプローチは強制しません——オブジェクトが何になれるかを宣言し、変容が起こるに任せます。
8. 二種類の透明性
構造的
UserInput → ValidatedUser → SavedUser → ActiveUser
変容の経路が型に見えています。
意味的
string $email // 名前がEmail検証を示唆
string $password // 名前がPassword検証を示唆
名前が意味を運びます。
構造と意味の両方で透明であることが、コードをそれ自身のドキュメントにします。
-
ヘラクレイトス 万物流転 Input → Being → Finalアリストテレス 可能態 Success\|Failure $being老子 無為 #[Be]宣言スピノザ 必然的存在 意味変数 フッサール 内在における超越 #[Input]+#[Inject]荘子 自己証明 $beenハイデガー 言葉は存在の家 クラス名が世界を構築 仏教 刹那滅 消滅と生成の繰り返し
ログ駆動開発 (LDD)
「かつて荘周は夢で胡蝶となった。(中略)はたして周が夢で胡蝶となったのか、それとも胡蝶が夢で周となっているのか。」 —荘子『斉物論』(紀元前4世紀頃)
注意: 本章はBe Frameworkが目指す「未来のビジョン」を記述したものです。現在のバージョンではすべてが実装されているわけではありません。
究極の透明性
Be Frameworkが目指すのは、コード、実行、ログ、仕様の全てにおいて究極の透明性があり、またその境界が曖昧になる世界です。
3つの透明性がもたらす可逆性
仕様書からコードを書き、実行してログが出力されるという一方通行の流れは当たり前のものです。しかし、Be Frameworkは以下の3つの透明性によって、完全な「可逆性(Reversibility)」の可能性を探ろうとしています。
- 構造的透明性 (Structural Transparency):
#[Be]属性によって、変容のフローがコードの構造そのものとして明示されています。これにより、静的解析だけでアプリケーションの全遷移図(Decision Graph)を描くことが可能です。
- 意味的透明性 (Semantic Transparency):
- 変数名は単なるラベルではなく、契約です。
$emailという名前はEmailクラスによる検証と思想的な定義(ALPSなど)を内包しています。これにより、変数名を見るだけでそのデータの「意味」と「保証」が分かります。
- 変数名は単なるラベルではなく、契約です。
- 実行透明性 (Execution Transparency):
- セマンティックログは、単なる「通過記録」ではなく、「なぜその決定に至ったか」という判断の根拠を全て記録します。
価値の無限ループ
この透明性が、AIによる「推測の排除」を実現します。
- Log: 人間(またはAI)が「あるべき物語」をログとして記述する。
- AI Construction: AIはそのログから、構造的・意味的透明性を頼りに、推測ではなく「作業」としてコードを組み立てる。
- Code: 生成されたコードは、時間的存在(Temporal Being)の定義になる。
- Execution: コードが実行され、契約通りのログが出力される。
- Verification: 出力されたログと、最初の物語(Log)が一致することを、実行透明性によって確認する。
このサイクルにより、「夢(Log)」と「現実(Code)」が相互に変換され続ける世界が完成します。
Log-Driven Development (LDD)
この可逆性がもたらす新しい開発手法が、Log-Driven Development (LDD) です。
“Code ⇔ Log ⇔ Specification”
TDD(テスト駆動開発)が「振る舞い(Doing)」を定義してから実装するように、LDDでは「物語(Log)」を定義してから実存(Code/Being)を生成します。
- Narrative First: 開発者はまず、「あるべき実行ログ(Semantic Log)」を書きます。これはシステムが辿るべき「物語」であり、アプリケーションを定義するDSL(ドメイン固有言語)そのものです。
```yaml
UserRegistration:
- Input: {email: “New User”, …} Becomes: ValidatedUser
- ValidatedUser: Becomes: RegisteredUser ```
- Specification Generation: ログから「仕様(どのような状態遷移が必要か)」が導き出されます。
- Code Generation: 仕様から、それを実現するための
#[Be]チェーンとクラス定義が自動生成されます。
未来のデバッグ
究極の透明性が実現された世界では、デバッグは「ログを読む」ことと同義になります。再現不可能なバグは存在しません。なぜなら、ログが完全な「実行可能な仕様書」となっており、そのログを流し込むだけで、システムは全く同じ変容プロセスを再現(Replay)できるからです。
結論: Code as Philosophy
冒頭で引用した「胡蝶の夢」はこう伝えています。
夢の中で胡蝶(蝶のこと)としてひらひらと飛んでいた所、目が覚めたが、はたして自分は蝶になった夢をみていたのか、それとも実は夢でみた蝶こそが本来の自分であって今の自分は蝶が見ている夢なのか
LDDでログがコードになりえるのはまさにこの胡蝶の夢で、Be Frameworkの完全な透明性が、ログとコードの意味の境界を曖昧にします。意味的ログはいわば、アプリケーション実行のDSLです。その時、思想が言葉になり、言葉がコードになり、コードが物語になる。
Be Frameworkにおけるプログラミングは、単にコンピュータに命令することではありません。 それは、デジタル空間に生命のような「時間的存在」を定義し、その変容が自ら物語(Log)を紡ぎ出す行為です。
Be Framework FAQ
0) 要旨(TL;DR)
Q. これは新しい”プログラミングパラダイム”ですか?
A. パターンは既存の問いに対するより良い答えです。パラダイムは問いそのものを変えます。
$emailは外部から検証されるのではなく、存在していること自体が正しさの証明です。意味変数も、自己決定も、自己証明も、コレオグラフィーも——個別の機能ではなく、「ドメインの自我を中心に世界を見る」という一つの視点からすべてが導出されます。
手続き型は HOW(どうやるか)を問い、オブジェクト指向は WHAT(それは何か)を問いました。存在指向は WHETHER(そもそも存在できるか)を問います。
2) 主要機能
Q5. #[Be]属性は何をしますか?
A. オブジェクトの運命(次に何になるか)を宣言します。
単一または複数の変容候補を指定し、実行時に$beingプロパティの型で継続先が自動選択されます。
詳しくは変容をご参照ください。
Q6. $beingプロパティの役割は何ですか?
A. 現在の存在が導く次の存在を保持します。
ユニオン型を使用することで、結果の全可能性を明示しています。
Q6.5. $beingや$beenは特別なプロパティですか?
A. いいえ、特別なプロパティではありません。
これらはフレームワークが魔法的に解釈するものではなく、単なる規約です。
becoming()関数はプロパティ名を読むのではなく、プロパティに宣言された型を見て次のクラスを選択します。
そのため、$beingや$beenという名前である必要はありません。public Success|Failure $result;のように別の名前でも同じように動作します。
ただし、ドキュメント・サンプル・設計思想に合わせてこの名前を使用することで、「変容先(being)」「完了の証跡(been)」が一目で分かるというメリットがあります。
Q7. 変容の連鎖はどのように制御されますか?
A. #[Be]属性の連鎖により、自動的に次の変容が決定されます。
複数の変容先が可能な場合は、$beingプロパティのユニオン型で表現し、実際に代入された型によって次の変容先が決まります。この仕組みにより、複雑なビジネスロジックも宣言的に表現できます。
Q8. 「意味変数」とは何ですか?
A. 変数名=意味+制約です。$emailは「有効なEmail」でなければ存在できません。
分散しがちな検証(controller/validator/docs)を型に統合します。
(詳細:意味変数)
Q8.5. 意味は違うが制約が同じ変数($userId、$authorIdなど)はどう扱いますか?
A. 共通の基底クラスやトレイトで制約を共有し、継承で意味を分離します。
// src/Semantic/Abstract/Id.php - 共通制約を持つ基底クラス
namespace App\Semantic\Abstract;
abstract readonly class Id {
public function __construct(public string $value) {
if (!preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}/', $value)) {
throw new InvalidIdException();
}
}
}
// src/Semantic/UserId.php - 意味変数として継承
readonly class UserId extends \App\Semantic\Abstract\Id {}
readonly class AuthorId extends \App\Semantic\Abstract\Id {}
// 使用例:変数名に意味が込められる
function updateArticle(UserId $userId, AuthorId $authorId) {
// $userIdと$authorIdは混同不可能
}
Q9. 「存在理由層(Reason)」とは何ですか?
A. ある存在が成立するための根拠——道具一式をひとつのオブジェクトにまとめます。
従来のDIでは依存が個別にバラバラに注入されるため、「この存在が成り立つ前提条件は何か」が見えにくくなります。Reasonは存在の根拠を意味で束ねるので、テスト時にはその前提条件を丸ごと差し替えられます。
(詳細:存在理由層)
Q10. $been(自己証明)は何のためですか?
A. その存在が完了した証跡(誰が・いつ・何を)を内在させます。
外部テストや監査ログと整合しやすくなります。
(詳細:最終オブジェクト)
4) 既存資産との統合
Q15. 既存のMVCアプリに導入できますか?
A. はい。UseCase層をBeで置き換え、Controllerからbecoming(new …Input)を呼びます。段階的な移行が可能です。
Q16. DBや外部APIはどこで使いますか?
A. Reasonに閉じ込めます。存在型はDBスキーマやAPIの都合から自由になり、存在の定義と技術的手段が分離されます。
Q17. フレームワークの依存関係は?
A. コアはPHP標準+DI(例:Ray.Di)を使用します。Laravel/Symfony等との統合はアダプタ経由で可能です。
Q18. 静的解析やIDE補完は効きますか?
A. 型が”状態”なので効果的に効きます。
ユニオン型で分岐が明示化され、補完も安全です。
6) マイグレーション
Q21. 既存コードの移行手順は何ですか?(最小ステップ)
A. 以下の手順をお勧めします:
- 代表ユースケースを1つ選びます
- 入力を
…Inputとして抽出します(内在のみ) - 変換先を
…(存在)として設計し、#[Be]を付与します - 外部依存をReasonに集約します
- Controllerから
becoming(new …Input)を呼びます - 意味変数へ検証を移管/例外を意味的に置換します
8) 例とスニペット
Q23. 典型フローの最小例はどのようなものですか?
A.
// 1) 入力(内在のみ)
#[Be(ValidatedUser::class)]
final readonly class UserInput {
public function __construct(public string $name, public string $email) {}
}
// 2) 存在(変容の瞬間)
final readonly class ValidatedUser {
public string $display;
public bool $isValid;
public function __construct(
#[Input] string $name,
#[Input] string $email,
#[Inject] NameFormatter $fmt,
#[Inject] EmailValidator $v
) {
$this->display = $fmt->format($name);
$this->isValid = $v->validate($email);
}
}
// 3) 実行(自己組織化)
$user = $becoming(new UserInput($name, $email));
// 結果: ValidatedUser オブジェクト
// $user->display: "フォーマット済みの名前"
// $user->isValid: true (有効なメールの場合)
10) さらに深く
Q. もっと深く知りたいのですが?
A. 高性能LLM(Fable 5)による中立的な三部作の評論があります。まずは3行のwhileループと15人の思想家 — Be Framework 評論からどうぞ。
これはBEAR.Sundayの全てのマニュアルページを一つにまとめたページです。
概要
真の航海とは、新しい風景を探すことではなく、新しい目を持つことである。
—マルセル・プルースト『囚われの女』(À la recherche du temps perdu 第5巻)1923年
DoingからBeingへ
まず、これを見てください
// 従来のユーザー削除方法
$user = User::find($id);
$user->delete();
// 異なるユーザー削除方法
$activeUser = User::find($id);
$deletedUser = new DeletedUser($activeUser);
DeletedUserって何?と思うかもしれません。 これまで考えてもみなかった方法で、プログラミングを考えてみましょう。
『何をするか』から『何であるか』へ
従来のプログラミングはDOING(何をするか)に着目します:
$user->validate();
$user->save();
$user->notify();
Be FrameworkはBEING(何であるか)に着目します:
$userInput = new UserInput($name, $email);
$validatedUser = new ValidatedUser($userInput);
$savedUser = new SavedUser($validatedUser);
前者はオブジェクトに対して「何をしろ」と指示します。 後者はオブジェクトが「どのような状態になるか」を表現します。
なぜこれが重要なのか
DOINGに着目すると:
- 実行前に「このアクションは可能か?」を毎回チェックする必要があります
- 様々なエラーケースに対処しなければなりません
- 不正な状態を防ぐための処理が常に必要です
BEINGに着目すると:
- 不正な状態のオブジェクトは最初から存在しません
- オブジェクトが存在すること自体が「正しい状態」の証明になります
- その時にできることだけに集中できます。できないことはそもそも実行できません
違いは型そのものにあります:
// 従来型:汎用的な型
function processUser(User $user) { }
// Be Framework:特定の存在状態
function processUser(ValidatedUser $user) { }
function saveUser(SavedUser $user) { }
function archiveUser(DeletedUser $user) { }
各型はただのデータではなく、オブジェクトの特定の状態を表しています。時間的な変化が型で表されるので、その時点で可能な操作だけが行えます。たとえば、削除済みのオブジェクトにさらに削除を命じることはできません。
なぜ「コントローラー」ではないのか?
従来のMVCフレームワークでは、コントローラーがアプリケーション全体の流れを制御します。しかし、システムが複雑になるにつれて、この「全てを制御しようとするアプローチ」は困難になります。
コントローラーは全てのモデルやコンポーネントに無制限にアクセスできる「全能の自由」を持っています。しかし制約がないということは、システム内のあらゆる手続きの整合性を自分で保つという「無限の責任」を負うことを意味します。
Be Frameworkは異なるアプローチを採ります。操作が目的のデータをつくり出すのではなく、種子のような単純な入力オブジェクトが他のオブジェクトと出会い、自然に成長し、最終オブジェクトへと自ら変容していきます。
Commander (司令官) から Gardener (庭師) へ:
- 司令官は部下(オブジェクト)に「動け」と命令します。しかし、システムが複雑になるほど、すべてを命令で制御し続けるのは困難です。
- 庭師は、植物に命令しません。ただ、水や光という環境を整えるだけです。
植物は他者を変えようとせず、環境を受け入れて自らを変容させ、あるべき姿に成ります。Be Frameworkも同じです。入力を与えると、自らが最終オブジェクトになるような環境を整えます。制御を手放し、自律的な変容に委ねる。これがBe Frameworkのコアコンセプトです。
このマニュアルで学べること
以下の新しいプログラミング手法を身につけることができます:
- 「何をするか」ではなく「何であるか」を設計する
- 不正な状態をチェックするのではなく、最初から作れないようにする
- オブジェクトを無理に変更するのではなく、自然な変容(自己変容)を表現する
- エラーを防ぐのではなく、正しい状態を信頼する
なぜ「DeletedUser」なのか?
冒頭の問いに戻りましょう。new DeletedUser($activeUser) は操作ではなく、変容です。ユーザーが「削除される」のではなく、$activeUserからDeletedUserという新しい存在が生まれるのです。型そのものが削除済みであることを証明しています。$statusフラグを確認する必要も、削除済みのユーザーに誤ってメソッドを呼ぶ危険もありません。これがBe Frameworkの本質です:状態遷移を既存オブジェクトへの操作ではなく、新しい型として表現する。
さあ、始めましょう
まず動かしてみたい方は? Getting StartedでHello Worldを体験するか、チュートリアルで実践的な例に挑戦できます。
概念から理解したい方は? 入力クラスに進んで、基礎からステップバイステップで学びましょう →
入力クラス
「私たちは自分で選択できない条件から始まり、そこから自分の存在を築く」
—ハイデガーの被投性(Geworfenheit)概念より(『存在と時間』1927年)
出発点
入力クラスは、Be Frameworkにおけるすべての変容の出発点です。
入力クラスにはオブジェクト自身が持つ要素だけが含まれ、外部依存がありません。これがオブジェクトの本質的な属性です。オブジェクトの内側にあるものなので、これを内在(Immanence)と呼びます。
基本構造
#[Be(ValidatedUser::class)] // 変容の運命
final readonly class UserInput
{
public function __construct(
public string $name, // 内在
public string $email // 内在
) {}
}
主要な特徴
純粋なアイデンティティ: 入力クラスはオブジェクトが何であるかだけを含みます。外部依存や複雑なロジックはありません。
ユースケースの起点: すべてのユースケースは固有の入力クラスを持ちます。
変容先(オブジェクトの運命): #[Be()]属性は、この入力が何になるかを宣言します。
読み取り専用プロパティ: すべてのプロパティは readonly です。入力クラスの値は変更されません。
例
単純なデータ入力
#[Be(OrderCalculation::class)]
final readonly class OrderInput
{
public function __construct(
public array $items, // 内在
public string $currency // 内在
) {}
}
複雑な構造化入力
#[Be(PaymentProcessing::class)]
final readonly class PaymentInput
{
public function __construct(
public Money $amount, // 内在
public CreditCard $card, // 内在
public Address $billing // 内在
) {}
}
変容の行き着く先、最終オブジェクトへ ➡️
最終オブジェクト
「あなたは私ではない。どうして私が魚の気持ちを知らないと分かるのか?」
—「あなたは魚ではない。どうして魚の気持ちが分かるのか」と問われた時に荘子が返した言葉 (『荘子』紀元前4世紀)
終着点
ユーザーにとって見えるのは、入力と最終オブジェクトだけです。入力クラスで始まった旅が、最終オブジェクトとして届く—これが変容の到達点です。
基本構造
final readonly class SuccessfulOrder
{
public string $orderId;
public string $confirmationCode;
public DateTimeImmutable $timestamp;
public string $message;
public BeenConfirmed $been; // 完了の証跡
public function __construct(
#[Input] Money $total, // 内在
#[Input] CreditCard $card, // 内在
#[Inject] OrderIdGenerator $generator, // 超越
#[Inject] Receipt $receipt // 超越
) {
$this->orderId = $generator->generate();
$this->confirmationCode = $receipt->generate($total);
$this->timestamp = new DateTimeImmutable();
$this->message = "注文確認: {$this->orderId}";
$this->been = new BeenConfirmed(
actor: $card->getHolderName(),
timestamp: $this->timestamp,
evidence: [
'total' => $total->getAmount(),
'payment_method' => $card->getType(),
'confirmation' => $this->confirmationCode
]
);
}
}
$beenプロパティはアプリケーションが定義する、ドメイン固有の型を持ちます。SuccessfulOrderにはBeenConfirmed、FailedOrderにはBeenRejected、DeletedUserならBeenDeletedというように命名します。何を「完了の証跡」とするかはドメインによって異なり、必要な証跡に応じてクラスを設計します。
入力クラスとは対照的に、最終オブジェクトはドメインの豊かさを完全に表現した存在です。内在が超越と出会い、変容を経て、これ以上変わる必要のない完全な状態に達しています。
時間的存在の完全性
Be Frameworkでは、オブジェクトの時間的存在を二つの軸で捉えます:
#[Be]: なりたい自分、向かう先(未来への方向性)$been: 完了した自分(過去完了の証跡)
$orderIdや$confirmationCodeはビジネス上の本質的な値です。一方$beenは、いつ・誰が・何を根拠に完了したかという証跡を記録します。
内側からの完全性
従来のプログラミングでは、オブジェクトが正しく処理されたかどうかを外部のテストが判定します。しかし最終オブジェクトは、自分が何であるかを自分自身の構造として持っています。何が入力され、何が起こり、何になったか—その全てが一つの存在の中に収まり、完了の証拠になっています。
入力クラスとの対比
| 入力クラス | 最終オブジェクト |
|---|---|
| 変容の出発点 | 変容の到達点 |
| ユーザーが提供するもの | ユーザーが受け取るもの |
| シンプルな構造 | 豊かで完全な状態 |
複数の最終的運命
オブジェクトはその性質によって複数の最終形態を持つことができます。ここで使っている$becomingは変容チェーンを起動する仕組みで、次章で説明します:
$order = $becoming(new OrderInput($items, $card));
if ($order instanceof SuccessfulOrder) {
echo $order->confirmationCode;
} else {
echo $order->message; // エラーメッセージ
}
設計者の仕事
入力と最終オブジェクトの間にある変容の仕組みを設計すること—それが設計者の責務です。ユーザーが触れるのは両端だけですが、その間の存在クラスがシステムの骨格になります。
最終オブジェクトは、自分が完了したことを内側から知っています。
道は1つではありません。変容で様々な道を学びます ➡️
変容
「空間と時間は独立に定義できない」
—アルベルト・アインシュタイン『一般相対性理論の基礎』(1916年)
時間とドメインは分割できない
アインシュタインが時間と空間の不可分性を発見したように、Be Frameworkでは時間とドメインは分割できない一つの実体です。承認プロセスには承認の時間が、決済には決済の時間があり、それぞれのドメインロジックが持つ固有の時間軸に沿って変容が自然に現れます。
不可逆的時間の流れ
オブジェクトの変容は時間の矢に沿った一方向の流れです。過去に戻ることも、同じ瞬間に留まることもできません:
// 時間 T0: 入力の誕生
#[Be(EmailValidation::class)]
final readonly class EmailInput { /* ... */ }
// 時間 T1: 第一変容(T0は既に過去)
#[Be(UserCreation::class)]
final readonly class EmailValidation { /* ... */ }
// 時間 T2: 第二変容(T1は記憶となる)
#[Be(WelcomeMessage::class)]
final readonly class UserCreation { /* ... */ }
// 時間 T3: 最終存在(すべての過去を内包)
final readonly class WelcomeMessage { /* ... */ }
各瞬間は二度と戻らず、新しい存在は前の形態をその内部に記憶として保持します。川が流れるように、時間は一方向にのみ流れます。
運命の自己決定
現実の生物と同様に、オブジェクトは内在と超越の相互作用によって、自身の運命を決定します。これは予め決められたルートを辿るのではなく、その瞬間の状況に応じた自然な変容です:
#[Be([ApprovalNotification::class, RejectionNotification::class])]
final readonly class ApplicationReview
{
public Approved|Rejected $being;
public function __construct(
#[Input] string $email, // 内在
#[Input] array $documents, // 内在
#[Inject] ReviewService $reviewer // 超越
) {
$result = $reviewer->evaluate($documents);
// 運命は今この瞬間に決まる
$this->being = $result->isApproved()
? new Approved($email, $result->getScore())
: new Rejected($email, $result->getReasons());
}
}
ApprovedとRejectedはReasonオブジェクトです。TutorialのEmergencyやObservationと同じ役割です。理由(Reason)が運命を決めます。判定結果とその根拠を持ち、$beingに代入されると、その型が次のクラスを決定します。コンストラクタ内で完結する判定ロジックはReason層に置きます。一方、生成後に呼び出される振る舞い—TutorialのassignER()のような遅延実行メソッド—はFinalクラスに持たせます。
型による継続
#[Be()]で指定された候補クラスのうち、現在のオブジェクトのpublicプロパティで#[Input]コンストラクタ引数を満たせるクラスが自動的に選択されます:
// ApplicationReviewの$beingがApproved型なので
// #[Input] Approvedがマッチするこのクラスが選択される
final readonly class ApprovalNotification
{
public function __construct(
#[Input] Approved $approval,
#[Inject] Mailer $mailer
) {
$mailer->send($approval->email, 'Approved! Score: ' . $approval->score);
}
}
$beingはこの自己決定パターンでよく使われるプロパティ名のコンベンションですが、フレームワークが要求する名前ではありません。どのpublicプロパティでもマッチングの対象になります。
自己組織化パイプライン
Unixパイプが単純なコマンドを組み合わせて強力なシステムを作るように、Be Frameworkは型付きオブジェクトを組み合わせて自然な変容の流れを作ります。
# Unix: テキストが流れる外部制御のパイプライン
cat access.log | grep "404" | awk '{print $7}' | sort | uniq -c
Unixではshellがパイプを制御しますが、Be Frameworkではオブジェクト自身が#[Be()]で運命を宣言します。コントローラーやオーケストレーターのような外部の制御は存在しません。
ヘラクレイトスは「流れているのが川だ」と言いました。川が流れるのではなく、流れそのものが川であるように、Be Frameworkのドメインは、終端に至るまで流れ続ける時間的存在です。
存在する理由のない存在はありません。存在理由層へ ➡️
存在理由層
「すべてのものには、それが存在するための理由がある」
—ライプニッツ『充足理由律』(1714年)
存在の理由
ExpressDeliveryが速達配送として成り立つのは、速達配送の能力を持っているからです。StandardDeliveryが通常配送として成り立つのは、通常配送の能力を持っているからです。この「なぜその存在でいられるのか」の根拠が、raison d’être(レーゾンデートル:存在理由)です。
存在理由層は、このraison d’êtreを一つのオブジェクトとして表現する設計パターンです。
final readonly class ExpressDelivery
{
public Fee $fee;
public function __construct(
#[Input] OrderData $order, // 内在
#[Inject] ExpressShipping $reason // 存在理由
) {
$this->fee = $reason->calculateFee($order->weight);
}
}
ExpressShippingがExpressDeliveryのraison d’êtreです。速達配送に必要な道具一式をまとめて提供します。
$beingとしての存在理由
存在理由オブジェクトは$beingプロパティとしても使えます。このとき、そのオブジェクトの型が変容先の判別根拠になると同時に、その存在様式に固有のメソッドも提供します。
final readonly class ExpressDelivery
{
public Fee $fee;
public function __construct(
#[Input] OrderData $order,
#[Input] ExpressShipping $being // 型が変容先を決定し、速達固有のメソッドを提供
) {
$this->fee = $being->calculateFee($order->weight);
}
}
final readonly class StandardDelivery
{
public Fee $fee;
public function __construct(
#[Input] OrderData $order,
#[Input] StandardShipping $being // 型が変容先を決定し、通常配送固有のメソッドを提供
) {
$this->fee = $being->calculateFee($order->weight);
}
}
ExpressShipping $beingという型そのものが、なぜExpressDeliveryになるのかの理由です。フレームワークはこの型を読み取り、対応する変容先を自動選択します。
どのReasonオブジェクトも#[Inject](超越の能力を提供)にも$being(運命を決定)にもなれます。違いはオブジェクト自体ではなく、使われ方にあります。ある文脈で#[Inject]として患者を評価するJTASProtocolが、別の文脈では$beingとして運命を決定することもできます。
存在理由クラスの定義
存在理由クラスは、特定の存在様式を実現するために必要なサービスをまとめたものです:
namespace App\Reason;
final readonly class ExpressShipping
{
public function __construct(
private PriorityCarrier $carrier,
private RealTimeTracker $tracker,
) {}
public function calculateFee(Weight $weight): Fee // 速達料金
{
return $this->carrier->expressFee($weight);
}
public function guaranteeDeliveryBy(Address $address): \DateTimeImmutable // 配達日保証
{
return $this->carrier->guaranteedDate($address);
}
public function realTimeTrack(TrackingId $id): TrackingStatus // リアルタイム追跡
{
return $this->tracker->realTimeStatus($id);
}
}
final readonly class StandardShipping
{
public function __construct(
private RegularCarrier $carrier,
private BatchTracker $tracker,
) {}
public function calculateFee(Weight $weight): Fee // 通常料金
{
return $this->carrier->standardFee($weight);
}
public function estimateDeliveryWindow(Address $address): DateRange // 配達期間の見積もり
{
return $this->carrier->estimateWindow($address);
}
}
個別注入との違い
存在理由層は#[Inject]を使います。では複数の#[Inject]をバラバラに使う場合と何が違うのでしょうか。
個別の注入:
public function __construct(
#[Input] OrderData $order,
#[Inject] PriorityCarrier $carrier,
#[Inject] RealTimeTracker $tracker,
#[Inject] InsuranceService $insurance,
#[Inject] DeliveryScheduler $scheduler
) {
// バラバラの道具を個別に使用
}
存在理由層:
public function __construct(
#[Input] OrderData $order,
#[Inject] ExpressShipping $reason // 関連道具がまとまった存在理由
) {
$this->fee = $reason->calculateFee($order->weight);
}
「ExpressDeliveryになるには何が必要か?」という問いに、存在理由オブジェクト一つが答えます。オブジェクト自身は「何になるか」を宣言し、存在理由は「どうやってその状態になるか」を実現します。
Potentialを返すReason
ここまでのReasonはFeeのような即時の値を返していました。しかし注文処理を考えてみましょう。在庫確保・決済・配送手配のすべてが成功してからコミットする必要があります。決済が失敗したのに在庫だけ確保されたままでは困ります。
このような一括実現が必要な場面で、Reasonは値の代わりにPotentialを返します。Potentialは遅延操作を保持するオブジェクトで、後からbe()で実現されます。このパターンは複数の外部操作をアトミックにコミットする必要があるときだけ使います。
Potential: 準備済み・未コミット
Reasonのメソッドは外部操作を準備し、Potentialを返します:
final class PaymentGateway
{
public function authorize(string $cardNumber, int $amount): PaymentCapture
{
$authCode = $this->api->authorize($cardNumber, $amount);
return new PaymentCapture(
$authCode,
$amount,
fn () => $this->api->capture($authCode, $amount),
);
}
}
PaymentCaptureはPotentialです。認証コードとキャプチャの遅延操作を保持しています。決済は認証済みですが、まだ確定していません。be()で確定します:
$capture = $gateway->authorize($cardNumber, $amount);
$capture->authorizationCode; // 即座に利用可能
$capture->be(); // キャプチャを確定
Moment: Potentialを保持する
ReasonからPotentialを受け取って保持するクラスをMoment(ヘーゲルの契機—全体の中でのみ意味を持つ不可欠な側面)と呼びます。Momentはフレームワークが提供するMomentInterfaceを実装します:
interface MomentInterface
{
public function be(): void;
}
final readonly class PaymentCompleted implements MomentInterface
{
public PaymentCapture $capture;
public function __construct(
#[Input] public string $cardNumber,
#[Input] public int $amount,
#[Inject] PaymentGateway $gateway,
) {
$this->capture = $gateway->authorize($cardNumber, $amount);
}
public function be(): void
{
$this->capture->be();
}
}
収束: FinalがMomentを実現する
複数のMomentがすべて揃う必要があるとき、Final Objectはそれらを受け取り、各Momentのbe()を呼びます。これは外部からの命令ではなく、自己完成です:
final readonly class OrderConfirmed
{
public string $orderId;
public string $status;
public function __construct(
public InventoryReserved $inventory,
public PaymentCompleted $payment,
public ShippingArranged $shipping,
) {
$this->inventory->be();
$this->payment->be();
$this->shipping->be();
$this->orderId = 'ORD-' . date('Ymd') . '-' . bin2hex(random_bytes(4));
$this->status = 'confirmed';
}
}
いずれかのMomentが生成できなければ(Reasonが失敗したため)、Final Objectは構築されません。すべてのMomentが存在すれば、be()がすべての遅延操作をコミットします。手動のロールバックフラグもネストされたtry-catchも不要です。
このパターンを使う場面
複数の外部操作をアトミックに成功させる必要があるときに、Potentialを返すReasonを使います。Reasonが即時の値を返す単純なケースでは不要です。
フレームワークの全体像はリファレンスへ ➡️
意味的ログ
「記録されるものは記憶となり、記憶されるものは真実となる」
—オーウェル『1984年』の概念より(1949年)
概要
従来のログ:
[INFO] User registered: alice@example.com
[INFO] Verification passed
[INFO] Insert into users table
意味的ログ:
{
"open": { "from": "UnverifiedEmail", "to": "RegisteredUser" },
"events": [
{ "type": "email_format_asserted", "context": { "email": "alice@example.com" } },
{ "type": "user_inserted", "context": { "userId": 42, "email": "alice@example.com" } }
],
"close": { "properties": { "userId": 42, "value": "alice@example.com" } }
}
従来のログは行単位のテキストが時系列に並ぶだけで、どの行が同じ操作に属するかは読み手の推測に委ねられます。
意味的ログでは、ひとつの変容がひとつのJSONに収まります。変容元と変容先、途中の出来事、最終プロパティ — 「何が何になり、なぜそうなったか」が型付きの構造化データとして記録され、JSONスキーマで検証できます。
Be Frameworkには二つの意味的記録の仕組みがあります。
$been— Finalオブジェクトが自分の来歴を保持する証明(proof)SemanticLoggerInterface— 階層的な操作を記録するログ(log)
| log | $been |
|
|---|---|---|
| 性質 | descriptive(記述) | constitutive(構成) |
| 視点 | 第三者(観測者) | 一人称 |
| 問い | 何が起きたか | なぜ今の私なのか |
| 文法 | doing | being |
| 役割 | 記録 | 証明 |
$been — 存在証明
FinalオブジェクトのコンストラクタでBeenをインジェクトし、with()で出来事を記録していくと、そのオブジェクトがなぜ今の状態にあるかの証明になります。
final class RegisteredUser
{
public readonly int $userId;
public readonly Been $been;
public function __construct(
#[Input] string $value,
#[Inject] EmailVerifier $verifier,
#[Inject] UserRepository $users,
#[Inject] Been $been,
) {
if (! $verifier->check($value)) {
throw new UnbecomingException('email format failed');
}
$this->userId = $users->insert(['email' => $value]);
$this->been = $been
->with(new EmailFormatAssertedContext(
email: $value,
))
->with(new UserInsertedContext(
userId: $this->userId,
email: $value,
));
}
}
Beenは#[Inject]でDIコンテナから受け取ります。受け取ったBeenにはフレームワークが変容開始時に記録した変容元・変容先の情報がすでに含まれています。開発者はwith()で、Finalオブジェクトの内部でしか知り得ない出来事 — メールを検証した、ユーザーを挿入した — を追記します。
イベントコンテキスト
Beenに渡すイベントはAbstractContextのサブクラスです。
final class EmailFormatAssertedContext extends AbstractContext
{
public const string TYPE = 'email_format_asserted';
public const string SCHEMA_URL = 'https://myvendor.example.com/schemas/email-format-asserted.json';
public function __construct(
public readonly string $email,
) {}
}
TYPEはログ上のイベント種別、SCHEMA_URLはそのイベントのJSONスキーマを指します。コンストラクタのプロパティがそのままJSONのcontextフィールドになります。
DDDでいうドメインイベント — ビジネス上「起きたこと」を表すオブジェクトです。Finalオブジェクトが完了までに経験した事実を、アプリケーション固有のイベントコンテキストとして定義します。
SemanticLoggerInterface — 階層的な操作記録
階層的な操作記録が必要な場合は、SemanticLoggerInterfaceを直接インジェクトします。
final class RegisteredUser
{
public function __construct(
#[Input] string $value,
#[Inject] UserRepository $users,
#[Inject] SemanticLoggerInterface $logger,
) {
// 意図を宣言(open)
$id = $logger->open(new DbTransactionContext(table: 'users'));
// 途中の出来事を記録(event)
$this->userId = $users->insert(['email' => $value]);
$logger->event(new RowInsertedContext(userId: $this->userId));
// 結果を記録(close)
$logger->close(new TransactionResultContext(committed: true), $id);
}
}
open/event/closeはKoriym.SemanticLoggerの階層構造をそのまま使います。意図(intent)→ 出来事(occurrences)→ 結果(result)の三層で、ひとまとまりの操作を記録できます。
$beenはFinalオブジェクトが何であるかの証明です。SemanticLoggerInterfaceは従来のログに近い、途中経過の詳細な記録です。通常は$beenで足ります。
変容の自動記録
$beenやSemanticLoggerInterfaceとは別に、変容そのものもフレームワークがopen/closeで自動記録します。開発者がこの記録コードを書く必要はありません。
出力されるJSONの全体像です。
{
"open": {
"type": "metamorphosis_open",
"context": {
"fromClass": "MyVendor\\MyApp\\UnverifiedEmail",
"beAttribute": "#[Be(RegisteredUser::class)]",
"immanentSources": {
"value": "MyVendor\\MyApp\\UnverifiedEmail::value"
},
"transcendentSources": {
"verifier": "MyVendor\\MyApp\\EmailVerifier",
"users": "MyVendor\\MyApp\\UserRepository",
"been": "Be\\Framework\\SemanticLog\\Been"
}
}
},
"events": [
{
"type": "email_format_asserted",
"context": { "email": "alice@example.com" }
},
{
"type": "user_inserted",
"context": { "userId": 42, "email": "alice@example.com" }
}
],
"close": {
"type": "metamorphosis_close",
"context": {
"properties": { "userId": 42, "value": "alice@example.com" },
"be": { "finalClass": "MyVendor\\MyApp\\RegisteredUser" }
}
}
}
openが変容の意図(何から何へ、どの材料で)、eventsが$been->with()で記録された出来事、closeが結果(最終プロパティと変容先)です。
ログからDSLへ
従来のログは実行の記録です。コードが走った後に生まれ、デバッグに使われ、やがて消えます。
このJSONは違います。実行の記録であると同時に、変容の仕様でもあり、存在の証明でもあります。どこから来て、どう成ったのか、何であるのかの記録です。「UnverifiedEmailがRegisteredUserになる過程でemail_format_assertedとuser_insertedが起きる」— これは過去の事実の記述としても、未来の期待の宣言としても読めます。しかも型付きの構造化データなので、AIが読み書きできるDSLとしても機能します。
記録、仕様、証明、DSL。この四つが同じJSONに重なるとき、JSONスキーマによるテスト並みの厳密な検証と、ログからコードを生成しコードからログを生成する循環が可能になりえます。
ログ駆動開発 (LDD)
「かつて荘周は夢で胡蝶となった。(中略)はたして周が夢で胡蝶となったのか、それとも胡蝶が夢で周となっているのか。」 —荘子『斉物論』(紀元前4世紀頃)
注意: 本章はBe Frameworkが目指す「未来のビジョン」を記述したものです。現在のバージョンではすべてが実装されているわけではありません。
究極の透明性
Be Frameworkが目指すのは、コード、実行、ログ、仕様の全てにおいて究極の透明性があり、またその境界が曖昧になる世界です。
3つの透明性がもたらす可逆性
仕様書からコードを書き、実行してログが出力されるという一方通行の流れは当たり前のものです。しかし、Be Frameworkは以下の3つの透明性によって、完全な「可逆性(Reversibility)」の可能性を探ろうとしています。
- 構造的透明性 (Structural Transparency):
#[Be]属性によって、変容のフローがコードの構造そのものとして明示されています。これにより、静的解析だけでアプリケーションの全遷移図(Decision Graph)を描くことが可能です。
- 意味的透明性 (Semantic Transparency):
- 変数名は単なるラベルではなく、契約です。
$emailという名前はEmailクラスによる検証と思想的な定義(ALPSなど)を内包しています。これにより、変数名を見るだけでそのデータの「意味」と「保証」が分かります。
- 変数名は単なるラベルではなく、契約です。
- 実行透明性 (Execution Transparency):
- セマンティックログは、単なる「通過記録」ではなく、「なぜその決定に至ったか」という判断の根拠を全て記録します。
価値の無限ループ
この透明性が、AIによる「推測の排除」を実現します。
- Log: 人間(またはAI)が「あるべき物語」をログとして記述する。
- AI Construction: AIはそのログから、構造的・意味的透明性を頼りに、推測ではなく「作業」としてコードを組み立てる。
- Code: 生成されたコードは、時間的存在(Temporal Being)の定義になる。
- Execution: コードが実行され、契約通りのログが出力される。
- Verification: 出力されたログと、最初の物語(Log)が一致することを、実行透明性によって確認する。
このサイクルにより、「夢(Log)」と「現実(Code)」が相互に変換され続ける世界が完成します。
Log-Driven Development (LDD)
この可逆性がもたらす新しい開発手法が、Log-Driven Development (LDD) です。
“Code ⇔ Log ⇔ Specification”
TDD(テスト駆動開発)が「振る舞い(Doing)」を定義してから実装するように、LDDでは「物語(Log)」を定義してから実存(Code/Being)を生成します。
- Narrative First: 開発者はまず、「あるべき実行ログ(Semantic Log)」を書きます。これはシステムが辿るべき「物語」であり、アプリケーションを定義するDSL(ドメイン固有言語)そのものです。
```yaml
UserRegistration:
- Input: {email: “New User”, …} Becomes: ValidatedUser
- ValidatedUser: Becomes: RegisteredUser ```
- Specification Generation: ログから「仕様(どのような状態遷移が必要か)」が導き出されます。
- Code Generation: 仕様から、それを実現するための
#[Be]チェーンとクラス定義が自動生成されます。
未来のデバッグ
究極の透明性が実現された世界では、デバッグは「ログを読む」ことと同義になります。再現不可能なバグは存在しません。なぜなら、ログが完全な「実行可能な仕様書」となっており、そのログを流し込むだけで、システムは全く同じ変容プロセスを再現(Replay)できるからです。
結論: Code as Philosophy
冒頭で引用した「胡蝶の夢」はこう伝えています。
夢の中で胡蝶(蝶のこと)としてひらひらと飛んでいた所、目が覚めたが、はたして自分は蝶になった夢をみていたのか、それとも実は夢でみた蝶こそが本来の自分であって今の自分は蝶が見ている夢なのか
LDDでログがコードになりえるのはまさにこの胡蝶の夢で、Be Frameworkの完全な透明性が、ログとコードの意味の境界を曖昧にします。意味的ログはいわば、アプリケーション実行のDSLです。その時、思想が言葉になり、言葉がコードになり、コードが物語になる。
Be Frameworkにおけるプログラミングは、単にコンピュータに命令することではありません。 それは、デジタル空間に生命のような「時間的存在」を定義し、その変容が自ら物語(Log)を紡ぎ出す行為です。
デモ
実際に動くコードで、Be Frameworkの概念を体感してみましょう。
Hello World デモ
最もシンプルな変容です。名前を持つInputが、挨拶を持つFinalになります。
HelloInput → Hello
(name) (greeting)
Input
#[Be([Hello::class])]
final readonly class HelloInput
{
public function __construct(
public string $name,
) {}
}
#[Be]属性は、このInputが「何になれるか」を宣言します。
Final
final readonly class Hello
{
public string $greeting;
public function __construct(
#[Input] string $name, // HelloInputから
#[Inject] Greeting $greeting, // DIコンテナから
) {
$this->greeting = "{$greeting->greeting} {$name}";
}
}
Reason(存在理由)
final class Greeting
{
public string $greeting = 'Hello';
}
名前がReasonという外部の力を借りて挨拶になります。ここではGreetingが’Hello’を提供しています。
使用例
$input = new HelloInput(name: 'World');
$final = ($becoming)($input);
echo $final->greeting; // "Hello World"
リンク
“Be, Don’t Do”
Getting Started
概念を学んだら、実際に動かしてみましょう。
要件
- PHP 8.4+
- Composer
インストール
composer create-project be-framework/skeleton my-project --stability dev
cd my-project
サンプルを実行
composer dev
出力:
Hello World
これだけです!コードを見てみましょう。
プロジェクト構造
bin/
└── be.php # CLIエントリポイント
src/
├── Input/
│ └── HelloInput.php # 出発点
├── Final/
│ └── Hello.php # 目的地
├── Reason/
│ └── Greeting.php # 超越的な能力
├── Semantic/
│ └── Name.php # 意味的変数
├── Exception/
│ └── EmptyNameException.php
└── Module/
└── AppModule.php # DI設定
コード
Input クラス
#[Be([Hello::class])]
final readonly class HelloInput
{
public function __construct(
public string $name
) {}
}
#[Be] 属性はこの入力が何になるか、自らの運命を宣言します。
Final クラス
final readonly class Hello
{
public string $greeting;
public function __construct(
#[Input] string $name,
#[Inject] Greeting $greeting,
) {
$this->greeting = "{$greeting->greeting} {$name}";
}
}
#[Input]は前の段階(HelloInput)からデータを受け取ります。#[Inject]は外部からの能力(超越)を受け取ります。
Reason クラス
final class Greeting
{
public string $greeting = 'Hello';
}
Greeting は超越 — 挨拶する力—を提供します。
変容(メタモルフォーシス)の実行
$injector = new Injector(new AppModule());
$becoming = new Becoming($injector, 'Be\\App\\Semantic');
$input = new HelloInput('World');
$hello = $becoming($input);
echo $hello->greeting; // "Hello World"
何が起きたのか?
HelloInput('World')
↓ Becoming が実行
Hello (Greeting が注入された状態)
→ "Hello World"
入力オブジェクトは何もしていません — 変容を通じて Hello になった(BEING)のです。
セマンティック検証を試す
bin/be.php を編集して空の名前を渡してみましょう:
$input = new HelloInput('');
再度実行:
composer dev
Semantic/Name.php が名前が空であることを検証するため、エラーメッセージが表示されます。空の名前は存在できないのです。
示された主要概念
| 概念 | この例では |
|---|---|
| 内在 | HelloInput の $name |
| 超越 | #[Inject] で注入された Greeting |
| 変容 | HelloInput → Hello の変換 |
| セマンティック検証 | Name.php が入力を検証 |
次のステップ
Being クラスと分岐を含むより完全な例に進む準備ができましたか? チュートリアル へ →
または概念を復習:
- Input Classes - 出発点
- Final Objects - 目的地
- Semantic Variables - ドメインオントロジー
Be Framework マニュアル
変容を通じて存在を理解する
概要
新しいパラダイム:存在指向プログラミングとの出会い
入力クラス
変容の出発点 — 外部に依存しない純粋なデータ
存在クラス
外部の力と出会い、変容する中間段階
最終オブジェクト
変容の目的地 — 完全に変容した存在
変容
時間に沿った変容と、オブジェクト自身による分岐
意味変数
変数名が意味と検証ルールを持つ仕組み
存在理由層
オブジェクトが成り立つための根拠と道具一式
意味例外
意味的例外と多言語エラーメッセージ
リファレンス
フレームワーク開発に必要なリソースとリンク集
FAQ
よくある質問と回答集
Be, Don’t Do
チュートリアル: 救急トリアージ
バイタルサインが患者の存在を「緊急」または「経過観察」として決定するトリアージシステムを構築します。
前提条件
- Getting Started を完了していること
- PHP 8.4+
- Be Framework の概要 の基本的な理解
はじめに
このチュートリアルでは、Be Framework の核心を示す救急トリアージシステムを構築します:オブジェクトは何かを「する」のではなく、何かに「なる」のです。
患者は「トリアージされる」のではありません。医学プロトコルという患者自身は持たない(=超越的な)知恵に基づいて、緊急症例または経過観察症例になるのです。
変容の流れ
PatientArrival(生のバイタルサイン)
↓ JTAS プロトコルが評価
TriageAssessment(蛹の段階)
↓ 運命が決定される
EmergencyCase または ObservationCase(最終的な存在)
ステップ 1: 存在の語彙を定義する
ロジックを書く前に、意味変数——このドメインで何が存在できるかの語彙——を定義します。
BodyTemperature
// src/Semantic/BodyTemperature.php
/**
* 30°C未満または45°Cを超えると、人間は生存できない。
* そのような値はセマンティックレベルで拒否される。
*/
final class BodyTemperature
{
#[Validate]
public function validate(float $bodyTemperature): void
{
if ($bodyTemperature < 30.0 || $bodyTemperature > 45.0) {
throw new LethalVitalException();
}
}
}
HeartRate
// src/Semantic/HeartRate.php
/**
* 20 bpm未満または250 bpmを超えると心停止または致死的不整脈を示す。
*/
final class HeartRate
{
#[Validate]
public function validate(int $heartRate): void
{
if ($heartRate < 20 || $heartRate > 250) {
throw new LethalVitalException();
}
}
}
これらは単なる検証ルールではありません。何が存在できるかの語彙を定義しています。この宣言的な基盤は、人間とAIの両方がドメインを理解するために読めるドキュメントとして機能します。(詳細は意味変数を参照。)
ステップ 2: 例外を定義する
バイタルサインが生存不可能な状態を示す場合、患者の存在は拒否されます:
// src/Exception/LethalVitalException.php
#[Message([
'en' => 'Vital signs indicate non-survivable conditions.',
'ja' => 'バイタルサインが生存不可能な状態を示しています。'
])]
final class LethalVitalException extends \DomainException
{
}
ステップ 3: Reason(超越)を定義する
JTASProtocol(Japan Triage and Acuity Scale)は開発者の恣意的なルールではありません。超越的な医学の知恵、つまり世界に独立して存在する客観的な知識を表します。Be Frameworkでは、このようなドメインロジックを第一級市民として扱います。注入可能でテスト可能、かつ明示的に表現されます。
// src/Reason/JTASProtocol.php
/**
* JTAS (Japan Triage and Acuity Scale) プロトコル
*
* 個々の患者や開発者から独立して存在する
* 超越的な医学の知恵。
*
* 注: 実際のJTASは5レベル存在します。
*/
final readonly class JTASProtocol
{
/** @return 'emergency'|'observation' */
public function assess(float $bodyTemperature, int $heartRate): string
{
if ($bodyTemperature >= 39.0 || $heartRate >= 120) {
return 'emergency';
}
return 'observation';
}
}
これがReason—変容を可能にする外部の力です。幼虫が蝶になるために環境条件が必要なように、私たちのデータもトリアージされた患者になるために JTASProtocol が必要です。
ステップ 4: Input クラスを作成
出発点—到着時の生のバイタルサイン:
// src/Input/PatientArrival.php
#[Be([TriageAssessment::class])]
final readonly class PatientArrival
{
public function __construct(
public float $bodyTemperature,
public int $heartRate
) {}
}
#[Be] 属性は運命を宣言します:この到着は TriageAssessment になります。
ステップ 5: Reasonオブジェクトを作成
これらのReasonオブジェクトは2つの可能な運命を表します。理由(Reason)が「これでいい」と運命を決めます:
// src/Reason/Emergency.php
final readonly class Emergency {} // 緊急
// src/Reason/Observation.php
final readonly class Observation {} // 経過観察
これらは中身のないクラスに見えますが、型そのものが意味を持ちます。Emergency は Observation とは根本的に異なる存在です。$beingに代入されると、Reasonの型が次のFinalクラスを決定します。
ステップ 6: Being クラスを作成
ここで変容が起こります。患者は中間状態にあり、最終形態はまだ決まっていません:
// src/Being/TriageAssessment.php
/**
* 生のバイタルサインが JTAS プロトコル(超越的な知恵)と出会い、
* 患者の運命が決定される。
*/
#[Be([EmergencyCase::class, ObservationCase::class])]
final readonly class TriageAssessment
{
public Emergency|Observation $being;
public function __construct(
#[Input] public float $bodyTemperature,
#[Input] public int $heartRate,
#[Inject] JTASProtocol $protocol
) {
$urgency = $protocol->assess($bodyTemperature, $heartRate);
$this->being = ($urgency === 'emergency')
? new Emergency()
: new Observation();
}
}
#[Inject] が JTASProtocol を持ち込みます—外部からの超越的な知恵です。$being プロパティ(Union型)がどの Final クラスが変容を受け取るかを決定します。「ステータスを設定する」のではなく、患者がその運命になります。
ステップ 7: Final クラスを作成
最終形態—それぞれ独自の能力を持ちます:
EmergencyCase
// src/Final/EmergencyCase.php
/**
* 最高優先度として存在する患者
*
* これは単なるステータスフラグではなくこの患者は緊急です。
* この型よって他にはない能力が与えられます。
*/
final readonly class EmergencyCase
{
public string $priority;
public string $color;
public function __construct(
#[Input] public float $bodyTemperature,
#[Input] public int $heartRate,
#[Input] public Emergency $being
) {
$this->priority = 'IMMEDIATE';
$this->color = 'RED';
}
/**
* EmergencyCase だけが ERを割り当てできます。
*/
public function assignER(): string
{
return "直ちに救急室1を確保。救急医を呼び出し。";
}
}
ObservationCase
// src/Final/ObservationCase.php
/**
* 安定として存在する患者。
* 緊急症例が処理される間、安全に待機できる。
*/
final readonly class ObservationCase
{
public string $priority;
public string $color;
public function __construct(
#[Input] public float $bodyTemperature,
#[Input] public int $heartRate,
#[Input] public Observation $being
) {
$this->priority = 'DELAYED';
$this->color = 'GREEN';
}
/**
* 経過観察症例は待合室に割り当てられる
*/
public function assignWaitingArea(): string
{
return "待合室へ移動。30分ごとにバイタル監視。";
}
}
各型は異なるメソッドを持ちます。EmergencyCase には assignER() が、ObservationCase には assignWaitingArea() メソッドが存在します。型によって能力が決定されます — 経過観察の患者に救急室を割り当てることはできません。
ステップ 8: 変容を実行
// bin/be.php
use Be\App\Input\PatientArrival;
use Be\App\Module\AppModule;
use Be\Framework\Becoming;
use Ray\Di\Injector;
$injector = new Injector(new AppModule());
$becoming = new Becoming($injector, 'Be\\App\\Semantic');
// 高熱の患者
$patient = new PatientArrival(bodyTemperature: 39.5, heartRate: 90);
$final = $becoming($patient);
echo $final->priority; // "IMMEDIATE"
echo $final->color; // "RED"
echo $final->assignER(); // "直ちに救急室1を確保..."
時間的存在
すべての存在は時間の中で変化し、Input から Being を経て Final へと変容します。
PatientArrival(39.5°C, 90 bpm)
↓ #[Be([TriageAssessment::class])]
TriageAssessment
├─ JTASProtocol->assess() が 'emergency' を返す
└─ $being = Emergency
↓ #[Be([EmergencyCase::class, ObservationCase::class])]
EmergencyCase($being が Emergency なので)
→ priority: IMMEDIATE
→ color: RED
→ assignER(): "直ちに救急室1を確保..."
生存不可能な存在
// 生存可能範囲外の体温
$invalid = new PatientArrival(bodyTemperature: 50.0, heartRate: 80);
try {
$becoming($invalid);
} catch (SemanticVariableException $e) {
echo $e->getErrors()->getMessages('ja')[0];
// "バイタルサインが生存不可能な状態を示しています。"
}
変容は拒否されます。致死的なバイタルサインを持つ患者は私たちのシステムに存在できません。
なぜこれが重要なのか
従来のアプローチ(Doing)
$patient = new Patient($temp, $hr);
if ($triageService->isEmergency($patient)) {
$patient->setStatus('emergency');
$this->erService->assign($patient);
}
問題点:
- 患者は無効な状態で存在できる
- ステータスはいつでも変更できる
erService->assign()はどの患者にも呼べる
Be Framework のアプローチ(Being)
$patient = new PatientArrival($temp, $hr);
$final = $becoming($patient);
// $final は EmergencyCase または ObservationCase である
// EmergencyCase だけが assignER() メソッドを持つ
$final->assignER(); // 型安全:EmergencyCase でのみ可能
利点:
- 生存不可能な状態は存在できない
- 型がステータスである(不変)
- 能力は存在に属する
型が能力を決定する。存在は行動に先立つ。
プロジェクト構造
src/
├── Being/
│ └── TriageAssessment.php # 中間段階
├── Exception/
│ └── LethalVitalException.php
├── Input/
│ └── PatientArrival.php # 入力
├── Module/
│ └── AppModule.php # DI設定
├── Final/
│ ├── EmergencyCase.php # 最終形態:緊急
│ └── ObservationCase.php # 最終形態:経過観察
├── Reason/
│ ├── Emergency.php # Reason: 運命を決定
│ ├── JTASProtocol.php # Reason: 超越的な知恵
│ └── Observation.php # Reason: 運命を決定
└── Semantic/
├── BodyTemperature.php # 何が存在できるか
└── HeartRate.php
核心
患者は「トリアージされる」のではなく、トリアージされた状態になります。EmergencyCase と ObservationCase は異なる能力を持つ異なる型です。一度変容すると、新たな変容なしにステータスは変更できません。
すべての存在は時間の中に存在し、常に変化していて決して静止することはありません。絶えることなく自我を超越したものと出会い、影響を受け、自らを形作っていきます。「在ることは、成ること。」
他のドメインでの変容
同じパターンがあらゆる場所に適用されています:
| ドメイン | Input | Being | Final | Reason |
|---|---|---|---|---|
| トリアージ | PatientArrival | TriageAssessment | Emergency/Observation | JTASProtocol |
| 醸造 | RawMaterials | Fermentation | PremiumSake/Vinegar | YeastCulture |
| 入国審査 | VisaApplication | ConsularReview | Resident/Visitor | ImmigrationLaw |
| 裁判 | Evidence | Trial | Guilty/Acquitted | PenalCode |
| 恒星進化 | GasCloud | Protostar | Star/BlackHole | PhysicsLaws |
このようにすべてのドメインに変容があります。すべての存在に理由があります。全ては時間的存在です。