illuminatech/enum-seeder

Allows easy creation of DB seeders for the dictionary (enum) type tables

1.0.3 2024-03-25 11:42 UTC

This package is auto-updated.

Last update: 2024-11-25 13:00:40 UTC


README

Laravel Enum Seeder


This extension allows easy creation of DB seeders for the dictionary (enum) type tables, such as statuses, types, categories and so on.

For license information check the LICENSE-file.

Latest Stable Version Total Downloads Build Status

Installation

The preferred way to install this extension is through composer.

Either run

php composer.phar require --prefer-dist illuminatech/enum-seeder

or add

"illuminatech/enum-seeder": "*"

to the "require" section of your composer.json.

Usage

Almost every project requires specification of so called 'dictionary' or 'enum' entities, such as statuses, types, categories and so on. It is not always practical to keep such data as PHP enums or class-base enums. Sometimes it has to be put into a database table. For example: when we need to provide ability for the system administrator to edit human-readable title or description of the particular category or status, or enable/disable particular records, or simply to keep the database integrity.

Obviously keeping dictionary (enum) in the database tables creates a problem of its synchronization. As our project evolves new categories and statuses may appear, and some may become obsolete. Thus, we need a tool, which allows updating of the data in the dictionary (enum) tables. This package provides such a tool.

The idea is in creation of the special kind of database seeder, which synchronizes particular enum table with the predefined data in the way, it could be invoked multiple times without creation of redundant records or breaking an integrity. You can create such seeder extending Illuminatech\EnumSeeder\EnumSeeder. For example:

<?php

namespace Database\Seeders;

use Illuminatech\EnumSeeder\EnumSeeder;

class ItemCategorySeeder extends EnumSeeder
{
    protected function table(): string
    {
        return 'item_categories';
    }
    
    protected function rows() : array
    {
        return [
            [
                'id' => 1,
                'name' => 'Consumer goods',
                'slug' => 'consumer-goods',
            ],
            [
                'id' => 2,
                'name' => 'Health care',
                'slug' => 'health-care',
            ],
            // ...
        ];
    }
}

// ...

use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    public function run()
    {
        // always synchronize all dictionary (enum) tables:
        $this->call(ItemCategorySeeder::class);
        $this->call(ItemStatusSeeder::class);
        $this->call(ContentPageSeeder::class);
        // ...
    }
}

With seeders defined in such way, you can invoke following command after each project update:

php artisan migrate --seed

As the result table 'item_categories' will always be up-to-date with the values from ItemCategorySeeder::rows(). In case you need to add a new item category, you can simply add another entry to the ItemCategorySeeder::rows() and run the seeder again. It will gracefully add the missing records, keeping already existing ones intact.

You can control the seeding options overriding methods from Illuminatech\EnumSeeder\ControlsWorkflow.

Heads up! Make sure you do not setup a sequence (autoincrement) for the primary key (id) of the dictionary (enum) table, otherwise EnumSeeder may be unable to properly handle its data synchronization.

The example of the database migration for the dictionary (enum) table:

<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateItemStatusTable extends Migration
{
    public function up()
    {
        Schema::create('item_statuses', function (Blueprint $table) {
            $table->unsignedSmallInteger('id')->primary(); // no sequence (autoincrement)
            $table->string('name');
            // ...
        });
    }
}

Tip: Remember that it is not mandatory for primary key field to be always an integer - you may use strings for it just as well, keeping your database records more human-readable. For example:

<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateItemStatusTable extends Migration
{
    public function up()
    {
        Schema::create('item_statuses', function (Blueprint $table) {
            $table->string('id', 50)->primary();
            $table->string('name');
            // ...
        });
    }
}

Processing of the obsolete records

By default Illuminatech\EnumSeeder\EnumSeeder does not deletes the records, which are no longer specified at rows() method, as the database may already contain the references to those records via foreign keys, and deleting enum value may cause a data loss. However, if you sure what you are doing, you can control the deletion feature via shouldDeleteObsolete() method. For example:

<?php

use Illuminatech\EnumSeeder\EnumSeeder;

class ContentPageSeeder extends EnumSeeder
{
    protected function table(): string
    {
        return 'content_pages';
    }
    
    protected function rows() : array
    {
        return [
            [
                'id' => 1,
                'name' => 'About Us',
                'slug' => 'about-us',
                'content' => '<div>...</div>',
            ],
            [
                'id' => 2,
                'name' => 'How it works',
                'slug' => 'how-it-works',
                'content' => '<div>...</div>',
            ],
            // ...
        ];
    }
    
    protected function shouldDeleteObsolete(): bool
    {
        return true; // always delete records from 'content_pages', which 'id' is missing at `rows()`
    }
}

Each time ContentPageSeeder will be called, it will delete all the records from 'content_pages', which 'id' is missing at rows() method declaration.

Note: Remember that you can specify a complex logic at shouldDeleteObsolete() to suite your needs. For example, you can allow deleting of obsolete rows for "local" environment, while forbidding it for "prod":

<?php

use Illuminatech\EnumSeeder\EnumSeeder;

class ItemStatusSeeder extends EnumSeeder
{
    protected function table(): string
    {
        return 'item_statuses';
    }
    
    protected function rows() : array
    {
        return [
            // ...
        ];
    }
    
    protected function shouldDeleteObsolete(): bool
    {
        return $this->container->environment('local'); // allows deletion of the records only in "local" environment
    }
}

Deletion is not the only way to deal with obsolete records. In order to keep the database integrity, it is better to simply mark the obsolete records as outdated, e.g. perform a "soft-delete". This can be achieved via shouldUpdateObsoleteWith() method. For example:

<?php

use Illuminatech\EnumSeeder\EnumSeeder;

class ItemStatusSeeder extends EnumSeeder
{
    protected function table(): string
    {
        return 'item_statuses';
    }
    
    protected function rows() : array
    {
        return [
            // ...
        ];
    }
    
    protected function shouldUpdateObsoleteWith(): array
    {
        // following attributes will be applied to the records, which 'id' is missing at `rows()`
        return [
            'deleted_at' => now(),
        ];
    }
}

Common data for the records creation

You may simplify rows() method for the particular seeder extracting common attributes in shouldCreateWith() method. It defines the default attribute values for each created record, unless it explicitly overridden by the entry from rows(). For example:

<?php

use Illuminatech\EnumSeeder\EnumSeeder;

class ItemCategorySeeder extends EnumSeeder
{
    protected function table(): string
    {
        return 'item_categories';
    }
    
    protected function rows() : array
    {
        return [
            [
                'id' => 1,
                'name' => 'Active Category',
                // no need to specify 'is_active' and 'created_at' all the time
            ],
            [
                'id' => 2,
                'name' => 'Inactive Category',
                'is_active' => false, // overrides the value from `shouldCreateWith()`
            ],
            // ...
        ];
    }
    
    protected function shouldCreateWith(): array
    {
        // applies following attributes per each new created record:
        return [
            'is_active' => true,
            'created_at' => now(),
        ];
    }
}

Updating of the existing records

Each time the enum seeder is executed it updates existing records with the actual data from rows() if they mismatch. You can disable the update defining shouldUpdateExisting() method. For example:

<?php

use Illuminatech\EnumSeeder\EnumSeeder;

class ItemCategorySeeder extends EnumSeeder
{
    protected function table(): string
    {
        return 'item_categories';
    }
    
    protected function rows() : array
    {
        return [
            // ...
        ];
    }
    
    protected function shouldUpdateExisting(): bool
    {
        return false; // disable existing records update
    }
}

However, most likely you will need to allow updating for some attributes, while disallow it for the others. For example, if you setup content pages, you probably want control whether particular page is active or not via seeder, while the content fields, which are edited by the administrator, should remain intact. This can be achieved using shouldUpdateExistingOnly() method. For example:

<?php

use Illuminatech\EnumSeeder\EnumSeeder;

class ContentPageSeeder extends EnumSeeder
{
    protected function table(): string
    {
        return 'content_pages';
    }
    
    protected function rows() : array
    {
        return [
            [
                'id' => 1,
                'is_active' => true,
                'slug' => 'about-us',
                'title' => 'About Us', // default value, will be applied only on creation
                'content' => '<div>...</div>', // default value, will be applied only on creation
            ],
            // ...
        ];
    }
    
    protected function shouldUpdateExistingOnly(): array
    {
        // only 'is_active' and 'slug' will be synchronized, while 'title' and 'content' remains intact
        return [
            'is_active',
            'slug',
        ];
    }
}

You may specify the attributes, which should be applied on each row update, using shouldUpdateExistingWith(). For example:

<?php

use Illuminatech\EnumSeeder\EnumSeeder;

class ContentPageSeeder extends EnumSeeder
{
    protected function table(): string
    {
        return 'content_pages';
    }
    
    protected function rows() : array
    {
        return [
            // ...
        ];
    }
    
    protected function shouldUpdateExistingWith(): array
    {
        // attribute values to be applied per each row update.
        return [
            'updated_at' => now(),
        ];
    }
}

Heads up! Methods shouldUpdateExistingOnly() and shouldUpdateExistingWith() take precedence over shouldUpdateExisting(). If, at least, one of them defined, return value of shouldUpdateExisting() will be ignored, and the records will be updated anyway.

Eloquent enum seeders

It might be more convenient for you to operate Eloquent models instead of plain tables for enum seeding. Manipulating data via active record models allows you to use its full features such as "events", "timestamps" and "soft-delete". You can setup a enum seeder for particular Eloquent model using Illuminatech\EnumSeeder\EloquentEnumSeeder. For example:

<?php

use App\Models\ItemCategory;
use Illuminatech\EnumSeeder\EloquentEnumSeeder;

class ItemCategorySeeder extends EloquentEnumSeeder
{
    protected function model(): string
    {
        return ItemCategory::class;
    }
    
    protected function rows(): array
    {
        return [
            [
                'id' => ItemCategory::CONSUMER_GOODS,
                'name' => 'Consumer goods',
                'slug' => 'consumer-goods',
            ],
            [
                'id' => ItemCategory::HEALTH_CARE,
                'name' => 'Health care',
                'slug' => 'health-care',
            ],
            // ...
        ];
    }
}

Heads up! Remember to disable Illuminate\Database\Eloquent\Model::$incrementing for the enum Eloquent model, otherwise EloquentEnumSeeder may be unable to properly handle its data synchronization. For example:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class ItemCategory extends Model
{
    /**
     * {@inheritdoc}
     */
    public $incrementing = false; // disable auto-increment

    // ... 
}

Heads up! In case you are using string values as a primary key for enum table, you should also adjust Illuminate\Database\Eloquent\Model::$keyType accordingly. For example:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class ItemCategory extends Model
{
    /**
     * {@inheritdoc}
     */
    public $incrementing = false; // disable auto-increment
    
    /**
     * {@inheritdoc}
     */
    protected $keyType = 'string';  // setup 'string' type for primary key

    // ... 
}