diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..cd4cbb0 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,6 @@ +# Exclude development files from Packagist dist archives +tests/ export-ignore +.github/ export-ignore +phpunit.xml export-ignore +phpstan.neon export-ignore +composer.lock export-ignore diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 57c43d5..9bcec3f 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -23,6 +23,8 @@ src/ ├── Console/ │ ├── GenerateCommand.php — generate single entity from JSON schema │ ├── GenerateAllCommand.php — generate all entities in config/entities/ +│ ├── MakeMiddlewareCommand.php — scaffold a middleware class (--auth for AuthMiddlewareInterface stub) +│ ├── MakeControllerCommand.php — scaffold a controller class with CRUD stubs │ ├── MigrateCommand.php — run pending migrations (main DB) │ ├── MigrateAllTenantsCommand.php — run migrations on every tenant DB │ ├── RollbackCommand.php — rollback last migration batch @@ -49,7 +51,8 @@ src/ │ ├── Router.php — GET/POST/PUT/DELETE route dispatch │ ├── Pipeline.php — immutable middleware chain runner │ └── Middleware/ -│ └── MiddlewareInterface.php — handle(Request, callable): Response +│ ├── MiddlewareInterface.php — handle(Request, callable): Response +│ └── AuthMiddlewareInterface.php — marker interface for auth middleware; attach identity via withAttribute() │ ├── Repository/ │ └── BaseRepository.php — auto-scoped CRUD + transactions @@ -67,7 +70,8 @@ src/ └── Resolver/ ├── HeaderTenantResolver.php — reads tenant from a request header ├── SubdomainTenantResolver.php — extracts tenant from subdomain - └── JwtTenantResolver.php — verifies Bearer JWT, extracts tenant claim + ├── JwtTenantResolver.php — verifies Bearer JWT, extracts tenant claim + └── SessionTenantResolver.php — reads tenant from injected session data or $_SESSION ``` --- @@ -137,6 +141,7 @@ Configured via `tenancy.resolver` in `application.yaml`. | `header` | `header_key` (default: `X-Tenant-ID`) | Reads the named HTTP header from the request context | | `subdomain` | `subdomain_depth`, `subdomain_min_parts` (default: 3) | Extracts the leading subdomain from the `host` context key. Set `subdomain_min_parts: 2` for two-part hosts like `acme.io` | | `jwt` | `jwt_public_key`, `jwt_algorithm` (default: `RS256`), `jwt_tenant_claim` (default: `tenant_id`) | Decodes and verifies a Bearer JWT from the `Authorization` header, then extracts the named claim | +| `session` | `session_key` (default: `tenant_id`) | Reads tenant from `$context['session']` (testable, injected) or falls back to PHP `$_SESSION` | Example for JWT: @@ -360,14 +365,16 @@ Output: ## CLI Commands -| Command | Key options | Description | -|--------------------------|------------------------------------|------------------------------------------------------| -| `generate ` | | Generate entity + repository from JSON schema | -| `generate:all` | `--config-dir` | Generate all schemas in `config/entities/` | -| `migrate` | `--dry-run` | Run pending migrations on the main database | -| `migrate:rollback` | `--dry-run` | Roll back the last migration batch | -| `migrate:all-tenants` | `--dry-run`, `--parallel N` | Run pending migrations on every active tenant DB | -| `tenant:create ` | `--name` | Onboard a new tenant | +| Command | Key options | Description | +|----------------------------|------------------------------------|------------------------------------------------------| +| `generate ` | `--migration` | Generate entity + repository from JSON schema | +| `generate:all` | `--config-dir` | Generate all schemas in `config/entities/` | +| `make:middleware ` | `--auth`, `--output` | Scaffold a middleware class (`--auth` for `AuthMiddlewareInterface` stub) | +| `make:controller ` | `--output` | Scaffold a controller class with CRUD stubs | +| `migrate` | `--dry-run` | Run pending migrations on the main database | +| `migrate:rollback` | `--dry-run` | Roll back the last migration batch | +| `migrate:all-tenants` | `--dry-run`, `--parallel N` | Run pending migrations on every active tenant DB | +| `tenant:create ` | `--name` | Onboard a new tenant | --- diff --git a/docs/CLEANUP.md b/docs/CLEANUP.md index a980d6a..7986a12 100644 --- a/docs/CLEANUP.md +++ b/docs/CLEANUP.md @@ -8,7 +8,9 @@ This document tracks deliberate gaps and deferred work in the current codebase. **SubdomainTenantResolver minimum parts are configurable.** The default requires 3-part hosts (`acme.example.com`). Set `tenancy.subdomain_min_parts: 2` in `application.yaml` to support two-part hosts like `acme.io`. Single-part hosts (e.g. `localhost`) always throw regardless of this setting. -**`JwtTenantResolver` is implemented.** Decodes and verifies a Bearer JWT, extracts a configurable claim (default: `tenant_id`). Configure via `tenancy.jwt_public_key`, `tenancy.jwt_algorithm`, and `tenancy.jwt_tenant_claim`. Session-based resolution is not yet implemented — add a `SessionTenantResolver` following the same pattern. +**`JwtTenantResolver` is implemented.** Decodes and verifies a Bearer JWT, extracts a configurable claim (default: `tenant_id`). Configure via `tenancy.jwt_public_key`, `tenancy.jwt_algorithm`, and `tenancy.jwt_tenant_claim`. + +**`SessionTenantResolver` is implemented.** Reads tenant from `$context['session']` when present (testable path) or falls back to PHP `$_SESSION`. Configure the key via `tenancy.session_key` (default: `tenant_id`). Use `tenancy.resolver: session` in `application.yaml`. --- diff --git a/docs/CONCEPT.md b/docs/CONCEPT.md index d7ca4aa..8c067e0 100644 --- a/docs/CONCEPT.md +++ b/docs/CONCEPT.md @@ -28,12 +28,13 @@ A static singleton that holds the current tenant ID for the lifetime of a reques Resolves and caches the PDO connection for the current tenant. In `shared` mode it returns a connection to the main DB. In `database` mode it connects to `{base_db}_{tenantId}`. Connections are pooled in a static registry; call `flush()` to clear the cache. ## TenantResolver -Extracts a tenant ID from request context. Three implementations ship: +Extracts a tenant ID from request context. Four implementations ship: - `HeaderTenantResolver` — reads a configurable header (default: `X-Tenant-ID`). Configure via `tenancy.header_key`. - `SubdomainTenantResolver` — extracts the leading subdomain from the host (e.g. `acme.example.com` → `acme`). Set `tenancy.subdomain_min_parts: 2` to support two-part hosts like `acme.io`. - `JwtTenantResolver` — decodes and verifies a Bearer JWT from the `Authorization` header, then extracts a configurable claim (default: `tenant_id`). Configure via `tenancy.jwt_public_key`, `tenancy.jwt_algorithm` (default `RS256`), and `tenancy.jwt_tenant_claim`. +- `SessionTenantResolver` — reads tenant from `$context['session']` when provided (useful in tests), otherwise falls back to PHP `$_SESSION`. Configure the key via `tenancy.session_key` (default: `tenant_id`). -Configured via `tenancy.resolver: header | subdomain | jwt` in `application.yaml`. Add new resolvers by implementing `TenantResolverInterface` and registering them in `TenantResolverFactory`. +Configured via `tenancy.resolver: header | subdomain | jwt | session` in `application.yaml`. Add new resolvers by implementing `TenantResolverInterface` and registering them in `TenantResolverFactory`. ## TenantService The intended entry point for tenant lifecycle operations: diff --git a/readme.md b/readme.md index 7a9621d..04603be 100644 --- a/readme.md +++ b/readme.md @@ -3,7 +3,7 @@ [![CI](https://github.com/vedavith/Entity-Forge/actions/workflows/php.yml/badge.svg)](https://github.com/vedavith/Entity-Forge/actions/workflows/php.yml) [![codecov](https://codecov.io/gh/vedavith/Entity-Forge/graph/badge.svg)](https://codecov.io/gh/vedavith/Entity-Forge) -**EntityForge** is a configuration-driven, multi-tenant SaaS framework built in PHP 8.4. +**EntityForge** is a configuration-driven, multi-tenant SaaS framework built in PHP 8.3+. It provides everything needed to build a scalable SaaS backend: JSON-driven code generation, two tenant isolation strategies, automated migrations, an HTTP routing layer with middleware pipeline, and a dependency injection container — all wired together through a single boot cycle. @@ -24,7 +24,7 @@ It provides everything needed to build a scalable SaaS backend: JSON-driven code ## Requirements -- PHP 8.4+ +- PHP 8.3+ - MySQL (PDO) - Composer @@ -165,6 +165,8 @@ $response->send(); | `migrate:rollback` | `--dry-run` | Roll back the last migration batch | | `migrate:all-tenants` | `--tenant `, `--parallel N`, `--dry-run` | Run pending migrations on every active tenant DB | | `tenant:create ` | `--name` | Onboard a new tenant | +| `make:middleware ` | `--auth`, `--output` | Scaffold a middleware class (use `--auth` for `AuthMiddlewareInterface` stub) | +| `make:controller ` | `--output` | Scaffold a controller class with CRUD stubs | `generate:all` uses a single `EntityGenerator` instance to guarantee monotonically ordered migration timestamps within a session. @@ -207,6 +209,7 @@ Configure via `tenancy.resolver` in `application.yaml`: | `header` | `header_key` (default: `X-Tenant-ID`) | Reads the named HTTP header from the request context | | `subdomain` | `subdomain_depth`, `subdomain_min_parts` (default: 3) | Extracts the leading subdomain from the `host` context key (`acme.example.com` → `acme`). Set `subdomain_min_parts: 2` for two-part hosts like `acme.io` | | `jwt` | `jwt_public_key`, `jwt_algorithm` (default: `RS256`), `jwt_tenant_claim` (default: `tenant_id`) | Decodes and verifies a Bearer JWT from the `Authorization` header, then extracts the named claim | +| `session` | `session_key` (default: `tenant_id`) | Reads the tenant ID from `$context['session']` (injected) or falls back to PHP `$_SESSION` | Add custom resolvers by implementing `TenantResolverInterface` and registering them in `TenantResolverFactory`. @@ -496,8 +499,6 @@ Auth runs before tenant resolution if the tenant ID is embedded in the token. Re ## Roadmap -- [ ] Session-based tenant resolver -- [ ] Artisan-style scaffolding for middleware and controllers - [ ] Official Packagist release ---