CMS Framework - v2.2.2

Custom Fields Developer Guide

Overview

Custom fields allow you to add additional data to content types in Digital Shopfront CMS. Unlike traditional meta tables, custom fields are stored as actual database columns for better performance and queryability.

Table of Contents

Understanding Custom Fields

Column-Based Storage

Digital Shopfront CMS stores custom fields as actual database columns, not in a meta table:

Traditional Approach (Meta Table):

posts table: id, title, content
meta table: post_id, meta_key, meta_value

Digital Shopfront CMS Approach (Columns):

posts table: id, title, content, rating, price, featured

Benefits

  • Better Performance: Direct column access, no joins required
  • Type Safety: Database-level type enforcement
  • Indexable: Can create indexes on custom fields
  • Queryable: Standard SQL queries work
  • Simpler Code: Access fields as model properties

Available Field Types

v1.1.0: Field types and column types are now backed by the FieldType and ColumnType enums for type safety. You can use either string values (for backwards compatibility) or enum instances. See developer/enums for the complete reference.

Text Field

Single-line text input.

use ArtisanPackUI\CMSFramework\Modules\ContentTypes\Enums\FieldType;
use ArtisanPackUI\CMSFramework\Modules\ContentTypes\Enums\ColumnType;

[
    'type' => FieldType::Text,          // or 'text'
    'column_type' => ColumnType::String, // or 'string'
]

Use Cases: Names, titles, short descriptions, URLs

Textarea Field

Multi-line text input.

[
    'type' => 'textarea',
    'column_type' => 'text',
]

Use Cases: Long descriptions, notes, addresses

Number Field

Numeric input.

[
    'type' => 'number',
    'column_type' => 'integer',  // or 'decimal', 'float'
]

Use Cases: Quantities, ratings, prices

Boolean Field

True/false toggle.

[
    'type' => 'boolean',
    'column_type' => 'boolean',
]

Use Cases: Featured flag, published status, enabled/disabled

Date Field

Date picker.

[
    'type' => 'date',
    'column_type' => 'date',  // or 'datetime', 'timestamp'
]

Use Cases: Event dates, deadlines, publish dates

Select Field

Dropdown selection.

[
    'type' => 'select',
    'column_type' => 'string',
    'options' => [
        'choices' => ['Option 1', 'Option 2', 'Option 3'],
    ],
]

Use Cases: Status, priority, type selection

Checkbox Field

Multiple selections.

[
    'type' => 'checkbox',
    'column_type' => 'json',
    'options' => [
        'choices' => ['Feature 1', 'Feature 2', 'Feature 3'],
    ],
]

Use Cases: Multiple options, feature flags

Registering Custom Fields

Via Service Provider

<?php

namespace App\Providers;

use ArtisanPackUI\CMSFramework\Modules\ContentTypes\Managers\CustomFieldManager;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        $customFieldManager = app(CustomFieldManager::class);

        $customFieldManager->registerField([
            'name' => 'Price',
            'key' => 'price',
            'type' => 'number',
            'column_type' => 'decimal',
            'description' => 'Product price in USD',
            'content_types' => ['products'],
            'options' => [
                'precision' => 10,
                'scale' => 2,
            ],
            'order' => 1,
            'required' => true,
            'default_value' => '0.00',
        ]);
    }
}

Via Filter Hook

addFilter('ap.contentTypes.registeredCustomFields', function ($fields) {
    $fields['rating'] = [
        'name' => 'Rating',
        'key' => 'rating',
        'type' => 'number',
        'column_type' => 'integer',
        'content_types' => ['products', 'reviews'],
        'options' => [
            'min' => 1,
            'max' => 5,
        ],
        'required' => false,
        'default_value' => 3,
    ];

    return $fields;
});

Via Database/API

use ArtisanPackUI\CMSFramework\Modules\ContentTypes\Models\CustomField;

$field = CustomField::create([
    'name' => 'SKU',
    'key' => 'sku',
    'type' => 'text',
    'column_type' => 'string',
    'content_types' => ['products'],
    'order' => 1,
    'required' => true,
]);

Column Types

String

For short text (up to 255 characters).

'column_type' => 'string'

Migration:

$table->string('field_name')->nullable();

Text

For long text (up to ~65,000 characters).

'column_type' => 'text'

Migration:

$table->text('field_name')->nullable();

Integer

For whole numbers.

'column_type' => 'integer'

Migration:

$table->integer('field_name')->nullable();

Decimal

For precise decimal numbers.

'column_type' => 'decimal'
'options' => ['precision' => 10, 'scale' => 2]

Migration:

$table->decimal('field_name', 10, 2)->nullable();

Boolean

For true/false values.

'column_type' => 'boolean'

Migration:

$table->boolean('field_name')->default(false);

Date

For dates without time.

'column_type' => 'date'

Migration:

$table->date('field_name')->nullable();

DateTime

For dates with time.

'column_type' => 'datetime'

Migration:

$table->datetime('field_name')->nullable();

JSON

For structured data.

'column_type' => 'json'

Migration:

$table->json('field_name')->nullable();

Migration Generation

Automatic Migration

When you create a custom field, a migration is automatically generated:

$customFieldManager = app(CustomFieldManager::class);

$field = $customFieldManager->createField([
    'name' => 'Price',
    'key' => 'price',
    'type' => 'number',
    'column_type' => 'decimal',
    'content_types' => ['products'],
    'required' => false,
]);

// Migration automatically created in database/migrations/

Generated Migration Example

<?php

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

return new class extends Migration
{
    public function up(): void
    {
        Schema::table('products', function (Blueprint $table) {
            $table->decimal('price', 10, 2)->nullable();
        });
    }

    public function down(): void
    {
        Schema::table('products', function (Blueprint $table) {
            $table->dropColumn('price');
        });
    }
};

Manual Migration Generation

Generate a migration manually:

$customFieldManager = app(CustomFieldManager::class);
$field = CustomField::where('key', 'price')->first();

$migrationPath = $customFieldManager->generateMigration($field, 'add');
// Returns: database/migrations/2025_01_13_120000_add_price_to_content_types.php

Running Migrations

# Run all pending migrations
php artisan migrate

# Rollback last migration
php artisan migrate:rollback

# Rollback all migrations
php artisan migrate:reset

Retrieving and Displaying Values

Direct Column Access

Access custom fields as regular model properties:

$product = Product::find(1);

// Get custom field value
$price = $product->price;
$rating = $product->rating;
$featured = $product->featured;

// Set custom field value
$product->price = 29.99;
$product->rating = 5;
$product->featured = true;
$product->save();

In Queries

Use custom fields in queries:

// Where clause
$products = Product::where('price', '>', 100)->get();

// Order by
$products = Product::orderBy('rating', 'desc')->get();

// Select specific fields
$products = Product::select('id', 'title', 'price', 'rating')->get();

// Multiple conditions
$products = Product::where('featured', true)
    ->where('price', '<', 50)
    ->orderBy('rating', 'desc')
    ->get();

In Blade Templates

<div class="product">
    <h2>{{ $product->title }}</h2>
    <p>Price: ${{ number_format($product->price, 2) }}</p>
    <p>Rating: {{ $product->rating }}/5</p>

    @if($product->featured)
        <span class="badge">Featured</span>
    @endif
</div>

With Eloquent Relationships

// Eager load with custom fields
$products = Product::with('categories')
    ->where('price', '<', 100)
    ->get();

// Filter relationships
$category = Category::find(1);
$products = $category->products()
    ->where('featured', true)
    ->orderBy('price', 'asc')
    ->get();

Data Migration

Changing Field Types

When changing a custom field's type, data migration may be needed:

Example: String to Integer

// Before: price stored as string "29.99"
// After: price should be decimal 29.99

// 1. Create new migration
Schema::table('products', function (Blueprint $table) {
    $table->decimal('price_new', 10, 2)->nullable();
});

// 2. Migrate data
Product::chunk(100, function ($products) {
    foreach ($products as $product) {
        $product->price_new = floatval($product->price);
        $product->save();
    }
});

// 3. Drop old column, rename new
Schema::table('products', function (Blueprint $table) {
    $table->dropColumn('price');
    $table->renameColumn('price_new', 'price');
});

Handling Null Values

// Set default for existing records
Product::whereNull('rating')->update(['rating' => 0]);

// Or in migration
Schema::table('products', function (Blueprint $table) {
    $table->integer('rating')->default(0)->change();
});

Converting JSON to Columns

// Before: metadata JSON {'color': 'red', 'size': 'large'}
// After: color and size columns

Schema::table('products', function (Blueprint $table) {
    $table->string('color')->nullable();
    $table->string('size')->nullable();
});

Product::chunk(100, function ($products) {
    foreach ($products as $product) {
        $metadata = $product->metadata;
        $product->color = $metadata['color'] ?? null;
        $product->size = $metadata['size'] ?? null;
        $product->save();
    }
});

Best Practices

1. Use Descriptive Keys

✓ 'key' => 'product_price'
✓ 'key' => 'event_start_date'
✗ 'key' => 'price1'
✗ 'key' => 'field_1'

2. Choose Appropriate Column Types

// For prices
'column_type' => 'decimal'  // Not 'string' or 'float'

// For flags
'column_type' => 'boolean'  // Not 'integer' or 'string'

// For dates
'column_type' => 'date'     // Not 'string'

3. Set Sensible Defaults

'default_value' => 0,        // For numbers
'default_value' => false,    // For booleans
'default_value' => '',       // For strings

4. Use Required Sparingly

'required' => false,  // Most fields should be optional

5. Add Descriptions

'description' => 'Product price in USD. Enter without currency symbol.',

6. Order Fields Logically

'order' => 1,  // Display order in forms
'order' => 2,
'order' => 3,

7. Index Heavy-Use Fields

// In migration
Schema::table('products', function (Blueprint $table) {
    $table->decimal('price', 10, 2)->nullable()->index();
    $table->boolean('featured')->default(false)->index();
});

Examples

Product Price

$customFieldManager->registerField([
    'name' => 'Price',
    'key' => 'price',
    'type' => 'number',
    'column_type' => 'decimal',
    'description' => 'Product price in USD',
    'content_types' => ['products'],
    'options' => ['precision' => 10, 'scale' => 2],
    'order' => 1,
    'required' => true,
    'default_value' => '0.00',
]);

Event Date

$customFieldManager->registerField([
    'name' => 'Event Date',
    'key' => 'event_date',
    'type' => 'date',
    'column_type' => 'datetime',
    'description' => 'When the event starts',
    'content_types' => ['events'],
    'order' => 1,
    'required' => true,
]);
$customFieldManager->registerField([
    'name' => 'Featured',
    'key' => 'featured',
    'type' => 'boolean',
    'column_type' => 'boolean',
    'description' => 'Show on homepage',
    'content_types' => ['products', 'posts'],
    'order' => 10,
    'required' => false,
    'default_value' => false,
]);

Product Rating

$customFieldManager->registerField([
    'name' => 'Rating',
    'key' => 'rating',
    'type' => 'number',
    'column_type' => 'integer',
    'description' => 'Product rating (1-5)',
    'content_types' => ['products'],
    'options' => ['min' => 1, 'max' => 5],
    'order' => 5,
    'required' => false,
    'default_value' => 3,
]);

Deleting Custom Fields

Via Manager

$customFieldManager = app(CustomFieldManager::class);
$customFieldManager->deleteField($fieldId);

// Automatically:
// 1. Removes column from all content type tables
// 2. Generates migration
// 3. Deletes field from database

Warning

Deleting a custom field permanently removes data. Always backup before deleting.

See Also