cybercog / laravel-love

Add Social Reactions to Laravel Eloquent Models. It lets people express how they feel about the content. Fully customizable Weighted Reaction System & Reaction Type System with Like, Dislike and any other custom emotion types. Do you react?

Home Page:https://komarev.com/sources/laravel-love

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

[Concept] Configurable model table name

antonkomarev opened this issue · comments

Motivation

To have more control over the data storage.

Use cases

Cross-database queries

Define schema/database name to enable cross-database queries. Be careful with it, maybe it will be better to use lower-level API instead of higher-level facades.

Related issue: #163

Different table naming convention

Redefine database table names to follow another naming convention or due to naming conflicts.

Tenant aware table name prefixes

API which provides reaction system. Each tenant data may be separated by table prefixes. Personally I prefer to separate it by schemas/databases.

Proposal

Introduce models database table mappings mechanism.

Things to consider

Performance loss

Introducing one more configurable value will decrease performance. Unit tests are running slower on ~2.5% with new feature because of resoling table name from the Laravel Config. This possibly could be partially solved by using static mappings registry, but it will increase system complexity. This could be revised in future versions.

Here is how static mappings registry might look like:

final class ModelTables
{
    private static $mappings = [];

    public static function register(array $mappings): void
    {
        self::$mappings = $mappings;
    }

    public static function get(string $defaultTableName): string
    {
        return $mappings[$defaultTableName] ?? $defaultTableName;
    }
}

It could be registered it in LoveServiceProvider or in AppServiceProvider:

private function registerLoveModelTables(): void
{
    ModelTables::register(Config::get('love.storage.database.tables'));
}

Method getTable in abstract model will look like:

protected function getTable(): string
{
    return ModelTables::get($this->table); 
}

Solution №1

Add getTable method to abstract Cog\Laravel\Love\Support\Database\Eloquent\Model which will resolve table names from the package config file. Mapping keys are default table names.

Mapping in config file will look like:

'storage' => [
    'database' => [
        'connection' => env('DB_CONNECTION', 'mysql'),
        'tables' => [
            'love_reacters' => 'custom_reacters',
            'love_reactants' => 'custom_reactants',
            'love_reaction_types' => null,
            'love_reactions' => null,
            'love_reactant_reaction_counters' => null,
            'love_reactant_reaction_totals' => null,
        ],
    ],
],

Abstract model will have a method:

protected function getTable(): string
{
    return Config::get("love.storage.database.tables.{$this->table}")
        ?? $this->table;
}

Migrations will look like:

public function up(): void
{
    $this->schema->create((new Reaction())->getTable(), function (Blueprint $table) {
        $table->bigIncrements('id');
        $table->unsignedBigInteger('reactant_id');
        $table->unsignedBigInteger('reacter_id');
        $table->unsignedBigInteger('reaction_type_id');
        $table->decimal('rate', 4, 2);
        $table->timestamps();

        $table
            ->foreign('reactant_id')
            ->references('id')
            ->on((new Reactant())->getTable())
            ->onDelete('cascade');
        $table
            ->foreign('reacter_id')
            ->references('id')
            ->on((new Reacter())->getTable())
            ->onDelete('cascade');
        $table
            ->foreign('reaction_type_id')
            ->references('id')
            ->on((new ReactionType())->getTable())
            ->onDelete('cascade');
    });
}

Pros

  • Very minor chance of breaking changes between versions
  • Config looks intuitive when you see list of all tables and their mapping values
  • If you adding custom model implementation - there is no need to change anything in config

Cons

  • ?

Solution №2

Add getTable method to abstract Cog\Laravel\Love\Support\Database\Eloquent\Model which will resolve table names from the package config file. Mapping keys are model class names.

Mapping in config file will look like:

'storage' => [
    'database' => [
        'connection' => env('DB_CONNECTION', 'mysql'),
        'tables' => [
            Reacter::class => 'custom_reacters',
            Reactant::class => 'custom_reactants',
            ReactionType::class => null,
            Reaction::class => null,
            ReactionCounter::class => null,
            ReactionTotal::class => null,
        ],
    ],
],

Abstract model will have a method:

protected function getTable(): string
{
    return Config::get('love.storage.database.tables.' . static::class)
        ?? $this->table; 
}

Migrations will look like:

public function up(): void
{
    $this->schema->create((new Reaction())->getTable(), function (Blueprint $table) {
        $table->bigIncrements('id');
        $table->unsignedBigInteger('reactant_id');
        $table->unsignedBigInteger('reacter_id');
        $table->unsignedBigInteger('reaction_type_id');
        $table->decimal('rate', 4, 2);
        $table->timestamps();

        $table
            ->foreign('reactant_id')
            ->references('id')
            ->on((new Reactant())->getTable())
            ->onDelete('cascade');
        $table
            ->foreign('reacter_id')
            ->references('id')
            ->on((new Reacter())->getTable())
            ->onDelete('cascade');
        $table
            ->foreign('reaction_type_id')
            ->references('id')
            ->on((new ReactionType())->getTable())
            ->onDelete('cascade');
    });
}

Pros

  • Config looks intuitive when you see list of all models and their table names

Cons

  • One more place for breaking changes on models renaming or moving
  • If you adding custom model implementation - you need change classname in config as well

Solution №3

Add getTable method to abstract Cog\Laravel\Love\Support\Database\Eloquent\Model which will resolve table names from the static registry class. Mapping keys are model class names.

Static registry class will look like:

final class Love
{
    public static $reactionsTableName = 'love_reactions';

    public static function setReactionsTableName(string $tableName): void
    {
        static::$reactionsTableName = $tableName;
    }

    public static function getReactionsTableName(): string
    {
        return new static::$reactionsTableName;
    }
}

In AppServiceProvider we will need to define each table name like this:

Love::setReactionsTableName('custom_reactions');
Love::setReactantsTableName('custom_reactants');
Love::setReactersTableName('custom_reacters');

Abstract model will have a method:

protected function getTable(): string
{
    return Love::getReactionsTableName(); 
}

Migrations will look like:

public function up(): void
{
    $this->schema->create(Love::getReactionsTableName(), function (Blueprint $table) {
        $table->bigIncrements('id');
        $table->unsignedBigInteger('reactant_id');
        $table->unsignedBigInteger('reacter_id');
        $table->unsignedBigInteger('reaction_type_id');
        $table->decimal('rate', 4, 2);
        $table->timestamps();

        $table
            ->foreign('reactant_id')
            ->references('id')
            ->on(Love::getReactantsTableName())
            ->onDelete('cascade');
        $table
            ->foreign('reacter_id')
            ->references('id')
            ->on(Love::getReactersTableName())
            ->onDelete('cascade');
        $table
            ->foreign('reaction_type_id')
            ->references('id')
            ->on(Love::getReactionTypesTableName())
            ->onDelete('cascade');
    });
}

Pros

  • ?

Cons

  • Not obvious configuration place

Solution №1 was implemented and released in Laravel Love 8.4.0.