mirror of
https://github.com/archtechx/tenancy.git
synced 2026-02-05 08:44:04 +00:00
Merge branch 'may25' into improve-url-generation
This commit is contained in:
commit
58712f53af
4 changed files with 121 additions and 13 deletions
97
CLAUDE.md
Normal file
97
CLAUDE.md
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Development Commands
|
||||
|
||||
### Testing
|
||||
- `composer test` - Run tests without coverage using Docker
|
||||
- `./t 'test name'` - Run a specific test
|
||||
|
||||
### Code Quality
|
||||
- `composer phpstan` - Run PHPStan static analysis (level 8)
|
||||
- `composer cs` - Fix code style using PHP CS Fixer
|
||||
|
||||
### Docker Development
|
||||
- `composer docker-up` - Start Docker environment
|
||||
- `composer docker-down` - Stop Docker environment
|
||||
- `composer docker-restart` - Restart Docker environment
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
**Tenancy for Laravel** is a multi-tenancy package that automatically handles tenant isolation without requiring changes to application code.
|
||||
|
||||
### Core Components
|
||||
|
||||
**Central Classes:**
|
||||
- `Tenancy` - Main orchestrator class managing tenant context and lifecycle
|
||||
- `TenancyServiceProvider` (NOT the stub) - Registers services, commands, and bootstrappers
|
||||
- `Tenant` (model) - Represents individual tenants with domains and databases
|
||||
- `Domain` (model) - Maps domains/subdomains to tenants
|
||||
|
||||
**Tenant Identification:**
|
||||
- **Resolvers** (`src/Resolvers/`) - Identify tenants by domain, path, or request data - this data comes from middleware
|
||||
- **Middleware** (`src/Middleware/`) - Middleware that calls resolvers and tries to initialize tenancy based on information from a request
|
||||
- **Cached resolvers** - Cached wrapper around resolvers to avoid querying the central database
|
||||
|
||||
**Tenancy Bootstrappers (`src/Bootstrappers/`):**
|
||||
- `DatabaseTenancyBootstrapper` - Switches database connections
|
||||
- `CacheTenancyBootstrapper` - Isolates cache by tenant
|
||||
- `FilesystemTenancyBootstrapper` - Manages tenant-specific storage
|
||||
- `QueueTenancyBootstrapper` - Ensures queued jobs run in correct tenant context
|
||||
- `RedisTenancyBootstrapper` - Prefixes Redis keys by tenant
|
||||
|
||||
**Database Management:**
|
||||
- **DatabaseManager** - Creates/deletes tenant databases and users
|
||||
- **TenantDatabaseManagers** - Database-specific implementations (MySQL, PostgreSQL, SQLite, SQL Server)
|
||||
- **Row Level Security (RLS)** - PostgreSQL-based tenant isolation using policies
|
||||
|
||||
**Advanced Features:**
|
||||
- **Resource Syncing** - Sync central models to tenant databases
|
||||
- **User Impersonation** - Admin access to tenant contexts
|
||||
- **Cross-domain redirects** - Handle multi-domain tenant setups
|
||||
- **Telescope integration** - Tag entries by tenant
|
||||
|
||||
### Key Patterns
|
||||
|
||||
**Tenant Context Management:**
|
||||
```php
|
||||
tenancy()->initialize($tenant); // Switch to tenant
|
||||
tenancy()->run($tenant, $callback); // Atomic tenant execution
|
||||
tenancy()->runForMultiple($tenants, $callback); // Batch operations
|
||||
tenancy()->central($callback); // Run in central context
|
||||
```
|
||||
|
||||
**Tenant Identification Flow:**
|
||||
1. Middleware identifies tenant from request (domain/subdomain/path)
|
||||
2. Resolver fetches tenant model from identification data
|
||||
3. Tenancy initializes and bootstrappers configure tenant context
|
||||
4. Application runs with tenant-specific database/cache/storage
|
||||
|
||||
**Route Middleware Groups:**
|
||||
All of these work as flags, i.e. middleware groups that are empty arrays with a purely semantic use.
|
||||
- `tenant` - Routes requiring tenant context
|
||||
- `central` - Routes for central/admin functionality
|
||||
- `universal` - Routes working in both contexts
|
||||
- `clone` - Tells route cloning logic to clone the route
|
||||
|
||||
### Testing Environment
|
||||
|
||||
Tests use Docker with MySQL/PostgreSQL/Redis. The `./test` script runs Pest tests inside containers with proper database isolation.
|
||||
|
||||
`./t 'test name'` is equivalent to `./test --filter 'test name'`
|
||||
|
||||
**Key test patterns:**
|
||||
- Database preparation and cleanup between tests
|
||||
- Multi-database scenarios (central + tenant databases)
|
||||
- Middleware and identification testing
|
||||
- Resource syncing validation
|
||||
|
||||
### Configuration
|
||||
|
||||
Central config in `config/tenancy.php` controls:
|
||||
- Tenant/domain model classes
|
||||
- Database connection settings
|
||||
- Enabled bootstrappers and features
|
||||
- Identification middleware and resolvers
|
||||
- Cache and storage prefixes
|
||||
|
|
@ -20,7 +20,7 @@ class PendingScope implements Scope
|
|||
/**
|
||||
* Apply the scope to a given Eloquent query builder.
|
||||
*
|
||||
* @param Builder<\Stancl\Tenancy\Contracts\Tenant&Model> $builder
|
||||
* @param Builder<Model> $builder
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -108,6 +108,12 @@ class TableRLSManager implements RLSPolicyManager
|
|||
|
||||
protected function generatePaths(string $table, array $foreign, array &$paths, array $currentPath = []): void
|
||||
{
|
||||
// If the foreign key has a comment of 'no-rls', we skip it
|
||||
// Also skip the foreign key if implicit scoping is off and the foreign key has no comment
|
||||
if ($foreign['comment'] === 'no-rls' || (! static::$scopeByDefault && $foreign['comment'] === null)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (in_array($foreign['foreignTable'], array_column($currentPath, 'foreignTable'))) {
|
||||
throw new RecursiveRelationshipException;
|
||||
}
|
||||
|
|
@ -115,15 +121,7 @@ class TableRLSManager implements RLSPolicyManager
|
|||
$currentPath[] = $foreign;
|
||||
|
||||
if ($foreign['foreignTable'] === tenancy()->model()->getTable()) {
|
||||
$comments = array_column($currentPath, 'comment');
|
||||
$pathCanUseRls = static::$scopeByDefault ?
|
||||
! in_array('no-rls', $comments) :
|
||||
! in_array('no-rls', $comments) && ! in_array(null, $comments);
|
||||
|
||||
if ($pathCanUseRls) {
|
||||
// If the foreign table is the tenants table, add the current path to $paths
|
||||
$paths[] = $currentPath;
|
||||
}
|
||||
} else {
|
||||
// If not, recursively generate paths for the foreign table
|
||||
foreach ($this->database->getSchemaBuilder()->getForeignKeys($foreign['foreignTable']) as $nextConstraint) {
|
||||
|
|
|
|||
|
|
@ -503,7 +503,7 @@ test('table rls manager generates relationship trees with tables related to the
|
|||
|
||||
// Add non-nullable comment_id foreign key
|
||||
Schema::table('ratings', function (Blueprint $table) {
|
||||
$table->foreignId('comment_id')->onUpdate('cascade')->onDelete('cascade')->comment('rls')->constrained('comments');
|
||||
$table->foreignId('comment_id')->comment('rls')->constrained('comments')->onUpdate('cascade')->onDelete('cascade');
|
||||
});
|
||||
|
||||
// Non-nullable paths are preferred over nullable paths
|
||||
|
|
@ -640,16 +640,29 @@ test('table rls manager generates queries correctly', function() {
|
|||
test('table manager throws an exception when encountering a recursive relationship', function() {
|
||||
Schema::create('recursive_posts', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('highlighted_comment_id')->constrained('comments')->nullable()->comment('rls');
|
||||
$table->foreignId('highlighted_comment_id')->nullable()->comment('rls')->constrained('comments');
|
||||
});
|
||||
|
||||
Schema::table('comments', function (Blueprint $table) {
|
||||
$table->foreignId('recursive_post_id')->constrained('recursive_posts')->comment('rls');
|
||||
$table->foreignId('recursive_post_id')->comment('rls')->constrained('recursive_posts');
|
||||
});
|
||||
|
||||
expect(fn () => app(TableRLSManager::class)->generateTrees())->toThrow(RecursiveRelationshipException::class);
|
||||
});
|
||||
|
||||
test('table manager ignores recursive relationship if the foreign key responsible for the recursion has no-rls comment', function() {
|
||||
Schema::create('recursive_posts', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('highlighted_comment_id')->nullable()->comment('no-rls')->constrained('comments');
|
||||
});
|
||||
|
||||
Schema::table('comments', function (Blueprint $table) {
|
||||
$table->foreignId('recursive_post_id')->comment('rls')->constrained('recursive_posts');
|
||||
});
|
||||
|
||||
expect(fn () => app(TableRLSManager::class)->generateTrees())->not()->toThrow(RecursiveRelationshipException::class);
|
||||
});
|
||||
|
||||
class Post extends Model
|
||||
{
|
||||
protected $guarded = [];
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue