Skip to content

feat(ModelsCommand): add model_connection_resolver hook for dynamic connection/schema setup - #1788

Open
IlyasMohetna wants to merge 2 commits into
barryvdh:masterfrom
IlyasMohetna:feat/model-connection-resolver
Open

IlyasMohetna wants to merge 2 commits into
barryvdh:masterfrom
IlyasMohetna:feat/model-connection-resolver

Conversation

@IlyasMohetna

Copy link
Copy Markdown

Summary

The ide-helper:models command has no way to configure the database connection before it introspects a model's table. This is a problem for any setup where the active connection or schema depends on runtime context , the most common case being multi-tenant apps using PostgreSQL schemas-per-tenant (e.g. via stancl/tenancy with PostgreSQLSchemaManager).

At artisan CLI time, no tenant is bootstrapped. The PostgreSQL connection's search_path stays on public, so getPropertiesFromTable() finds no columns and silently produces empty docblocks for every tenant model.

The existing model_hooks / ModelHookInterface doesn't solve this. It was designed to add information to the docblock after introspection has already run. By the time run() is called, the schema lookup has already failed.


This PR adds a model_connection_resolver config option that accepts a class implementing the new ModelConnectionResolverInterface. The interface has two methods:

  • resolve(Model $model) called before getPropertiesFromTable(), so you can set the right connection, search_path, or any other context
  • after(Model $model) called after getPropertiesFromTable(), so you can revert whatever you changed
// config/ide-helper.php
'model_connection_resolver' => App\Support\IdeHelper\TenantConnectionResolver::class,
<?php

namespace App\Support\IdeHelper;

use Barryvdh\LaravelIdeHelper\Contracts\ModelConnectionResolverInterface;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\DB;

class TenantConnectionResolver implements ModelConnectionResolverInterface
{
    private ?string $tenantSchema = null;

    public function resolve(Model $model): void
    {
        // Only act on tenant models
        if (! str_starts_with(get_class($model), 'App\\Models\\Tenant\\')) {
            return;
        }

        $schema = DB::connection($model->getConnectionName() ?? config('database.default'))
            ->selectOne(
                "SELECT schema_name FROM information_schema.schemata
                 WHERE schema_name NOT IN ('public','information_schema','pg_catalog','pg_toast')
                   AND schema_name NOT LIKE 'pg_%'
                 ORDER BY schema_name LIMIT 1"
            )?->schema_name;

        if ($schema !== null) {
            $this->tenantSchema = $schema;
            DB::connection($model->getConnectionName() ?? config('database.default'))
                ->statement("SET search_path TO \"{$schema}\"");
        }
    }

    public function after(Model $model): void
    {
        if ($this->tenantSchema === null) {
            return;
        }

        DB::connection($model->getConnectionName() ?? config('database.default'))
            ->statement('SET search_path TO public');

        $this->tenantSchema = null;
    }
}

The feature is opt-in ; the config key defaults to null and the command behaviour is unchanged for anyone who doesn't set it.

The resolver is instantiated once per command run (not once per model), so the implementor can cache expensive lookups (like fetching a schema name) internally.

Type of change

  • New feature (non-breaking change which adds functionality)
  • This change requires a documentation update

Checklist

  • Existing tests have been adapted and/or new tests have been added
  • Update the README.md
  • Code style has been fixed via composer fix-style

Closes #1141
Related: #1552

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Table columns not discovered in multi-tenant databases

1 participant