glueful / extension-contracts
Shared cross-extension contracts for the Glueful ecosystem: tenancy, payment, and email-template seams.
Requires
- php: ^8.3
- glueful/framework: ^1.71.0
Requires (Dev)
- phpstan/phpstan: ^1.0
- phpunit/phpunit: ^10.5
- squizlabs/php_codesniffer: ^3.6
README
Shared cross-extension contracts for the Glueful ecosystem. This package is intentionally small: interfaces, readonly value objects, and constants only.
Soft Binding Rule
Only implementers bind shared contract IDs in the container. Consumers resolve a
contract with has() ? get() : <inline fallback> and never bind defaults under a
shared contract ID. That avoids boot-order and last-wins container surprises.
Tenancy
CurrentTenantResolver::tenantUuid() returns the active tenant UUID or ''.
The empty string is a valid single-store sentinel only when the consuming
extension is not running in tenant mode. In tenant mode, a bound resolver
returning '' means missing tenant context and the consumer must fail closed
unless it is running an explicitly named system/maintenance path.
TenantTableRegistry lets tenant-aware extensions register tenant-owned tables
without writing into another extension's config.
TenantRuntimeReadiness reports whether the host can resolve tenant-owned
requests safely (isReady()) and how (mode():
none | bootstrap_default | full_resolution). FullTenantResolutionReadiness
is a capability seam bound only when full domain/path/header resolution is
active — its container presence is how a readiness composite upgrades the mode.
TenantProvisioner stands up the first tenant + active owner membership
through a neutral seam. provisionDefault() is idempotent by caller-supplied
uuid (a crash-then-retry reuses the same tenant); hasAnyTenant() lets a
consumer refuse to provision over a pre-existing install.
TenantEnforcementProbe is the read-side view of the tenant-owned table
registry (isRegistered() / registeredTables()), so a finalization gate can
prove every owned table is registered in the serving process.
TenantContextRunner runs a callable as a given tenant, as the system channel,
or for each active tenant — the seam behind seed, sync, and background workers.
TenantScope resolves the current tenant uuid for raw-SQL consumers,
fail-closed: null when tenancy is inactive, the uuid when on, and
TenantContextRequiredException when on-but-empty. Builder paths are
auto-scoped by the tenancy guard/hook; raw PDO bypasses both, so raw consumers
use this to decide whether to append a tenant_uuid predicate.
TenantAdministration provides neutral tenant lifecycle and membership
operations. Implementations own transition, role, and final-active-owner
invariants so callers cannot bypass them.
TenantDomainAdministration manages normalized tenant host mappings while
keeping DNS verification separate from operator-controlled activation.
TenantResolutionProbe lets an activation flow verify one public host through
the real resolver and tenant-validation pipeline without bypassing its safety
checks.
TenantRequestMiddleware is the neutral request-time delegate for resolver
profiles. A host can expose an inert proxy before the tenancy implementation is
enabled, then delegate without importing the implementation's middleware class.
TenantProvisioningRunner runs a callable in the privileged context of a
provisioning tenant. The normal TenantContextRunner correctly refuses
non-active tenants, so seeders that materialize starter content before
activation use this narrowly-scoped runner instead of a weakened check.
Payments
PaymentCollector starts a payment for a PayableReference and must be
idempotent per (type, id): repeated calls return or refresh the same logical
intent.
PaymentConfirmationHandler is the provider-to-owner seam. Payment providers
dispatch verified successful payments only, with PaymentConfirmation::amount
in integer minor units. The payable owner still compares amount and currency
against its own record before transitioning business state.
EmailTemplateRegistry lets any extension declare mail templates as data —
EmailTemplateDefinition (key, label, default subject/body, owner) with
EmailTemplatePlaceholder metadata (name, description, sample) that drives
admin chips and test-sends. Implementations must enforce the collision rule:
re-registering a key is allowed only for the same owner; a different owner
claiming an existing key throws at boot. Registrant extensions soft-resolve
the registry (has() ? register : skip), so a missing email channel degrades
to "templates simply aren't registered".