Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -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
27 changes: 17 additions & 10 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
```

---
Expand Down Expand Up @@ -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:

Expand Down Expand Up @@ -360,14 +365,16 @@ Output:

## CLI Commands

| Command | Key options | Description |
|--------------------------|------------------------------------|------------------------------------------------------|
| `generate <Entity>` | | 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 <id>` | `--name` | Onboard a new tenant |
| Command | Key options | Description |
|----------------------------|------------------------------------|------------------------------------------------------|
| `generate <Entity>` | `--migration` | Generate entity + repository from JSON schema |
| `generate:all` | `--config-dir` | Generate all schemas in `config/entities/` |
| `make:middleware <Name>` | `--auth`, `--output` | Scaffold a middleware class (`--auth` for `AuthMiddlewareInterface` stub) |
| `make:controller <Name>` | `--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 <id>` | `--name` | Onboard a new tenant |

---

Expand Down
4 changes: 3 additions & 1 deletion docs/CLEANUP.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.

---

Expand Down
5 changes: 3 additions & 2 deletions docs/CONCEPT.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
9 changes: 5 additions & 4 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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

Expand Down Expand Up @@ -165,6 +165,8 @@ $response->send();
| `migrate:rollback` | `--dry-run` | Roll back the last migration batch |
| `migrate:all-tenants` | `--tenant <id>`, `--parallel N`, `--dry-run` | Run pending migrations on every active tenant DB |
| `tenant:create <id>` | `--name` | Onboard a new tenant |
| `make:middleware <Name>` | `--auth`, `--output` | Scaffold a middleware class (use `--auth` for `AuthMiddlewareInterface` stub) |
| `make:controller <Name>` | `--output` | Scaffold a controller class with CRUD stubs |

`generate:all` uses a single `EntityGenerator` instance to guarantee monotonically ordered migration timestamps within a session.

Expand Down Expand Up @@ -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`.

Expand Down Expand Up @@ -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

---
Expand Down
Loading