Skip to content

サービス ​

Lase が提供するサービスクラスの一覧です。すべて CodebaseJp\Lase\Services 名前空間に属します。

SubscriptionService ​

サブスクリプションの作成・更新を担当します。

createSubscription(Tenant $tenant, Plan $plan, Collection $options) ​

テナントに対してサブスクリプションを作成します。

  • 悲観的ロックでテナントをロックし、競合状態を防止
  • 現在のプランよりダウングレードする場合、利用量が新しい上限を超えていないかチェック
  • SubscriptionQuota・SubscriptionFeature・GrantedQuota・GrantedFeature を自動同期
  • DB トランザクション内で実行

recordUsage(string $tenantId, string $metric, int $value) ​

利用を記録します。サブスクリプションの上限まで SubscriptionUsage に加え、溢れた分を GrantedUsage(恒久枠)に加えます。合計の上限を超える場合は例外を投げ、何も記録しません。

  • テナントの行をロックして記録するため、同じテナントの記録が並行しても加算は失われません

usageSummary(Tenant $tenant) / setUsage(string $tenantId, string $metric, string $scope, int $value) ​

管理画面のご利用状況で、利用上限と利用数を確かめ、記録の誤りを直すために使います。

  • usageSummary() は、契約の枠(現在のサブスクリプション)と恒久枠(GrantedQuota・GrantedUsage)の上限と利用数を、指標ごとに返します。読み取りだけで、行は作りません
  • setUsage() は、scope(subscription / granted)の利用数を指定した値にし、前後の値を返します。上限は変えません。テナントの行をロックしてから直すため、同時に記録される利用とぶつかりません。契約の無いテナントの契約の利用数は直せません
  • 管理画面からは、全権限の管理者だけが直せます(PUT /api/admin/tenants/{id}/usages)。理由と前後の値は操作ログ(ADMIN_UPDATE_USAGE)に残ります

releaseUsage(string $tenantId, string $metric, int $value, CarbonInterface $recordedAt) ​

recordUsage() で記録した利用のうち、実際には使わなかった分を取り消します。たとえば送信を始める時点で件数分を記録し、送信に失敗した分をあとから返す、という使い方をします。

  • recordUsage() と逆の順に、GrantedUsage から返し、残りを SubscriptionUsage から返します。合計が同じなら、はじめから少なく記録した場合と同じ内訳になります
  • GrantedUsage から返すのは、今月のサブスクリプションの上限を使い切っているときだけです。GrantedUsage は月をまたいで残るため、上限を使い切っていない月に返すと前月までの分を返してしまいます。ただし GrantedUsage には月ごとの内訳がないため、上限を使い切った月に今月恒久枠を使った数より多く返すと、前月までの分も返ります
  • 使用量より多くは返しません。戻り値は実際に取り消した数です
  • $recordedAt(記録した日時)が現在のサブスクリプションの月より前なら何もせず 0 を返します。月が替わると使用量は 0 から数え直し、前月分は従量課金として Senter に送り済みのためです。月の途中のプラン変更では使用量を引き継ぐため、同じ月なら返します
php
// 送信を始めるときに件数分を記録する
$service->recordUsage($tenant->id, 'EMAIL_SENT_COUNT', $count);

// 配信結果が確定したら、送信できなかった分を返す
$service->releaseUsage($tenant->id, 'EMAIL_SENT_COUNT', $failedCount, $startedAt);

RegistrationPlanResolver ​

申込みフォームから登録するテナントに付けるプランの規則(RegistrationPlanRule)を決めます。

resolve(string $tenantCode, ?string $partnerId): ?RegistrationPlanRule ​

有効な規則を priority の小さい順(同じなら作った順)に見て、最初に合った規則を返します。どれにも合わなければ null です(呼ぶ側はトライアルのプランにします)。規則のプランが有効でないときは、その規則を飛ばして次を見ます。詳しくは 運用 を参照してください。

BusinessCalendar ​

組織の暦(営業する曜日・休日・終業時刻)で、営業日と期限を求めます。営業日は、営業する曜日のうち、休日でない日です。暦は組織の設定の「営業日と休日」で決めます。

php
$calendar = BusinessCalendar::forTenant($tenantId);
$calendar->isBusinessDay(Carbon::parse('2026-11-03'));      // 休日なら false
$calendar->deadline($now, 2);                               // 2営業日目の終業時刻(0 ならその営業日の終業時刻)
$calendar->addBusinessDays($date, -1);                      // 営業日で足し引きする(時刻はそのまま)
$calendar->deadlineFromValue('2026-11-30');                 // 日付ならその日の終業時刻、日時ならそのまま

組織を引かずに作ることもできます(new BusinessCalendar([1, 2, 3, 4, 5], '18:00', ['2026-11-03']))。

AuthService ​

認証周りのロジックを担当します。アカウントの有効化・パスワードリセット・トークン発行などを処理します。

TenantSuspensionService ​

テナントの緊急停止と解除を担当します。管理画面からの手動操作と、将来の自動停止(不正検知など)の両方から同じ経路を通すために切り出しています。

suspend(Tenant $tenant, string $reason, ?string $adminUserId, ?Request $request) ​

テナントを緊急停止します。保存により tenant:{code} のキャッシュが破棄されるため、次のリクエストから遮断されます。

  • $reason は運用の記録用です。テナント側の画面には出しません
  • $adminUserId を渡した場合のみアクティビティログ(ADMIN_SUSPEND_TENANT)を記録します
  • 契約は維持されるため、サブスクリプションの更新は停止中も行われます

resume(Tenant $tenant, ?string $adminUserId, ?Request $request) ​

緊急停止を解除します。停止の履歴はアクティビティログに残るため、テナント側の停止情報(suspended_at など)は消します。

php
// 操作者がいない自動停止では、監査の user_id が必須のためアクティビティログは残りません
$service->suspend($tenant, '不審なアクセスを検知');

TenantScreeningService ​

テナント登録時に、生成AI(OpenAI の Responses API)で登録内容の疑わしさ(ブランドの騙り・迷惑登録)を審査します。公開の登録フォームからの登録で、登録の確定後にキュー(Jobs\ScreenTenantRegistration)から呼ばれます。管理画面から作ったテナントは審査しません。

  • 判定は記録するだけで、テナントを自動で止めません。 疑わしいもの(REVIEW / ERROR)は管理画面の「登録審査」に出し、運営がテナントの詳細画面で緊急停止するかを決めます
  • AI に渡すのはテナント名・テナントコード・メールアドレスのドメインだけです。担当者名・電話番号・メールアドレスのローカル部は送りません
  • 審査対象はデータ(JSON)として渡し、出力は JSON Schema の構造化出力で受けます。登録内容に埋め込まれた AI への指示には従わせず、その記述があれば点数によらず REVIEW にします
  • 呼び出しの失敗・タイムアウト・形の崩れた応答は素通しにせず ERROR(運営の確認)として記録します

screen(Tenant $tenant): ?TenantScreening ​

審査して tenant_screenings に記録します。lase.tenant_screening.enabled が無効なら何もせず null を返します。

decide(int $riskScore, array $categories): TenantScreeningVerdict ​

点数が lase.tenant_screening.review_threshold(既定 50)以上、または観点に PROMPT_INJECTION を含めば REVIEW、それ以外は PASS です。

TenantUserRoleService ​

テナントユーザーへの役割の割り当てを担当します(役割は1人1つ)。

assign(TenantUser $tenantUser, ?string $tenantRoleId) ​

役割を割り当てます(null なら役割を外します)。権限はリクエストをまたいでキャッシュされるため、変更後に TenantAuthorizer::invalidateCache() を呼びます。

アプリ側で独自のユーザー更新処理を持つ場合も、キャッシュの破棄漏れを防ぐためにこのサービスを使ってください。役割を変更してよいかの判定は TenantUserPolicy::changeRole と TenantRolePolicy::assign が持ちます(Lase 内ではテナント側・管理側の TenantUserController::update が両方このサービスを使います。管理側はポリシーによる確認を行わないため、オーナーの保護はテナント側の経路にのみ効きます)。

php
$this->authorize('changeRole', $tenantUser);

if ($tenantRoleId !== null) {
    $this->authorize('assign', TenantRole::findOrFail($tenantRoleId));
}

$service->assign($tenantUser, $tenantRoleId);

アプリが自分で登録した役割は TenantRole::findByCode() で引いて割り当てます。

php
$service->assign($tenantUser, TenantRole::findByCode('snaps.admin')->id);

TenantLoginTokenService ​

メインドメインでログイン済みのユーザーを、テナントのサブドメインへログインさせるワンタイムトークン(1分間有効)を扱います。POST /api/auth/tenants/{tenant}/issue-login-token と POST /api/auth/login-by-token はこのサービスを使います。

issue(User $user, Tenant $tenant, bool $remember = false, ?string $actor = null): string ​

トークンを発行します。緊急停止中のテナントでは TenantSuspendedException、ログインする資格が無ければ AuthorizationException を投げます。

  • $actor を省略した場合: テナントにアクティブに所属(TenantUser)するユーザーだけが対象です
  • $actor を指定した場合: config('lase.tenant_login_actors') に登録した TenantLoginActor の判定で、所属していないユーザーもログインさせます(設定を参照)

consume(string $token, ?Tenant $tenant): ?array ​

トークンを使い切り、['user' => User, 'remember' => bool] を返します。無効なトークン、別テナント向けのトークン、発行後に資格を失ったユーザー(所属の利用停止や立場の判定を通らなくなった場合)は null を返します。

AnalyticsService ​

管理画面向けの統計データを集計します。

  • テナント数・ユーザー数・お問い合わせ数のサマリー
  • 日次・月次の時系列データ生成

RecaptchaService ​

Google reCAPTCHA v3 のトークン検証を行います。

php
$recaptchaService->verify(string $token): bool

SenterService ​

外部課金サービス「Senter」との API 通信を担当します。

  • サービスプラン・オプション・アドオン情報の取得
  • 請求書の取得
  • 未請求利用量の取得
  • Stripe 連携(顧客作成・SetupIntent・支払方法管理)

ensureTenantRegistered(Tenant $tenant) ​

テナントが Senter に顧客と契約を持つようにします。どのテナントも Senter に顧客と契約を持つ前提のため、Senter と連携する前に作ったテナントなど、欠けているものだけをここで作ります。

  • サービス利用(請求のもと)は作りません。請求が始まるのは、テナントがプランを選んだとき(SubscriptionService::createSubscription()。Senter にサービス利用が無ければ、更新ではなく新しく作ります)
  • 確かめたテナントは1日覚えておき、そのあいだは Senter に問い合わせません
  • 同時に補わないよう、テナントごとにロックします(キャッシュのドライバーがロックに対応している必要があります)

契約の画面の API の前では、tenant.senter ミドルウェアがこれを呼びます。

ActivityLogService ​

操作ログの記録を担当します。

record(BackedEnum $type, int $userId, ?int $tenantId, ?Request $request = null) ​

アクティビティログを1件記録します。

  • $type: ログの種別(ActivityLogType Enum の値)
  • $userId: 操作を行ったユーザーの ID
  • $tenantId: テナント ID(テナントコンテキスト外では null)
  • $request: HTTP リクエスト(省略時は IP・UA が記録されない。スケジューラー・キュー等の非 HTTP コンテキストから呼ぶ場合は省略可)

recordForTenant(BackedEnum $type, string $summary, array $metadata = [], ?Request $request = null) ​

ログイン中の人が、ログイン中の組織で行った操作を記録します(組織の操作ログに出ます)。操作した人と組織はリクエストから取り、どちらかが無ければ記録しません。$summary(何に対して行ったか)は metadata.summary に入り、組織の操作ログの画面に出ます。

AiEmbeddingService ​

ドキュメントをベクトル化してデータベースに保存し、AIチャットの RAG(検索拡張生成)を実現するサービスです。

インデックスの作成は php artisan lase:index-docs コマンドで実行します。

主なメソッド ​

メソッド説明
chunkText()Markdown をチャンクに分割する。Embeddings API に依存しない純粋関数
isIndexed()同一内容で既にインデックス済みかを返す
indexDocument()チャンク分割 → Embedding 生成 → DB 保存を行う
findRelevantChunks()クエリに近いチャンクを類似度の高い順に返す
pruneExcept()指定ソース一覧に含まれない孤立チャンクを削除する

チャンク分割の方針 ​

  • 先頭の YAML フロントマターは除去し、本文のみを対象にする
  • 見出し(#〜###)単位に分割する。コードブロック(``` / ~~~)内の # は見出しとして扱わない
  • 各チャンクの先頭に見出しパス(例: ログイン > パスワードを忘れた場合)を付与し、チャンク単体でも文脈が分かるようにする
  • 上限文字数を超えるセクションは段落単位で分割し、チャンク間に chunk_overlap 文字分の重複を持たせる

検索の仕様(ハイブリッド検索) ​

ベクトル検索と全文検索の結果を RRF(Reciprocal Rank Fusion) で融合します。

text
score = Σ 1 / (60 + 各検索での順位)

ベクトル検索は意味的な近さに強い一方、製品名・エラーコードのような固有語の完全一致に弱いという弱点があります。全文検索(MySQL の ngram パーサ)を併用することで、たとえば「E4021」のような語がベクトル的に一致しないチャンクも拾えるようになります。hybrid_enabled を false にするとベクトル検索のみになります。

ベクトルは正規化(ノルム1)して保存するため、類似度計算は内積のみで済みます。similarity_threshold を下回るチャンクは無関係とみなして除外し、無関係な文書がプロンプトへ混入するのを防ぎます。クエリ側の Embedding は query_cache_ttl 秒間キャッシュされます。

WARNING

InnoDB の全文検索インデックスは未コミットの行を参照できません。インデックス作成と同一トランザクション内で検索した場合、全文検索側は0件となりベクトル検索のみに縮退します(テストで RefreshDatabase を使う場合が該当します)。

WARNING

Embedding 生成に失敗した場合は例外を投げます(ゼロベクトルを保存してインデックスを黙って壊さないため)。indexDocument() は Embedding 生成を完了させてから既存チャンクを差し替えるため、API 失敗時に既存インデックスを失うことはありません。

ベクトルの保存形式 ​

embedding は正規化済みベクトルを float32 で並べた BLOB です。JSON 文字列に比べて次の効果があります(実測値)。

1536次元 JSON512次元 float32
1チャンクあたり32,794 B2,048 B
デコード(1000件)77.4 ms8.2 ms

次元数の削減は text-embedding-3-* の Matryoshka 表現を利用しており、1536→512 程度では検索精度の低下はごく僅かです。合わせて 約1/16のサイズ・約9.5倍のデコード速度になります。

embedding_model / dimensions を各行に保持しているため、モデルや次元数を変更すると:

  • 既存チャンクは SQL の段階で検索対象から除外されます(PHP 側で0点にするのではなく)
  • isIndexed() が false を返すため、--force なしで自動的に再インデックスされます

設定 ​

config/lase.php の ai セクションで調整できます。

キー既定値説明
embedding_modeltext-embedding-3-smallEmbedding 生成に使うモデル
embedding_dimensions512ベクトルの次元数。0 でモデル既定値(次元数指定に非対応のモデル向け)
embedding_batch_size501リクエストにまとめるチャンク数
chunk_max_length1500チャンクの上限文字数
chunk_min_length20これ以下のチャンクは破棄する
chunk_overlap100分割時に重複させる文字数
top_k5検索で取得するチャンク数
similarity_threshold0.3この類似度を下回るチャンクは除外する
hybrid_enabledtrue全文検索を併用し RRF で融合する
candidate_multiplier4融合前に各検索から取得する候補数の倍率(top_k × この値)
query_cache_ttl3600クエリ Embedding のキャッシュ秒数(0 で無効)