ca-santos/laravel-support

General-purpose, domain-organized helper library for Laravel applications

Maintainers

Package info

github.com/ca-santos/laravel-support

pkg:composer/ca-santos/laravel-support

Transparency log

Statistics

Installs: 6

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v2.0.0 2026-09-02 01:51 UTC

This package is auto-updated.

Last update: 2026-09-02 01:57:50 UTC


README

Laravel Support

A collection of small, dependency-light helpers for things almost every Laravel project ends up writing by hand: string/array/date/number formatting, casting request-style input, safe redirects, money and enum utilities, building Eloquent eager-load constraints, generating migrations at runtime, and hydrating plain PHP objects from arrays (including nested objects, Carbon dates, Collections and enums).

Everything is a plain static call — no service container bindings, no facades to fake in tests, no configuration to publish. This package only adds what Laravel doesn't already give you: nothing here duplicates a native Str, Arr, Number, or Eloquent method — where one already exists, use it directly instead.

use CaueSantos\Support\Helpers\StringHelper;
use CaueSantos\Support\Helpers\ArrayHelper;
use CaueSantos\Support\Helpers\DateHelper;

StringHelper::toSnakeSlug('My Cool Title');              // "my_cool_title"
ArrayHelper::isListOfArrays([['id' => 1], ['id' => 2]]); // true
DateHelper::formatUkDate('2024-03-05');                  // "05/03/2024"

Table of contents

Requirements

  • PHP 8.2+
  • illuminate/support 9.x through 12.x (works with Laravel 9 through 12)

Installation

composer require ca-santos/laravel-support

The service provider (CaueSantos\Support\ServiceProvider) is auto-discovered by Laravel — there is nothing to register or publish.

How this package is organized

Every class named XyzHelper lives under CaueSantos\Support\Helpers and exposes only public static methods — use the class and call it, nothing to instantiate:

use CaueSantos\Support\Helpers\StringHelper;

StringHelper::randomStr(12);

A handful of classes live directly under CaueSantos\Support because they aren't stateless utility helpers — traits meant to be used on your own classes, small value objects, or tools with their own constructor/state (object hydration, migration generation). Those are documented in their own sections below.

StringHelper

CaueSantos\Support\Helpers\StringHelper — string generation and formatting that Laravel's own Str class doesn't cover.

use CaueSantos\Support\Helpers\StringHelper;

StringHelper::toSnakeSlug('My Cool Title');           // "my_cool_title"
StringHelper::toSnakeSlug('My Cool Title', '-');      // "my-cool-title"

StringHelper::randomStr(10);                          // e.g. "qQqqOaTImp" (letters only — Str::random() mixes in digits)
StringHelper::lowerRandomStr(16);                     // e.g. "wowzultbfpokgxaf"

StringHelper::formatTags('Hello {{name}}!', ['name' => 'Ada']); // "Hello Ada!"
StringHelper::formatTags('Hi [name]', ['name' => 'Ada'], fn ($tag) => "[{$tag}]"); // "Hi Ada"

StringHelper::initials('John Doe'); // "JD"
StringHelper::initials('Ada Lovelace Byron', 3); // "ALB"

StringHelper::toDottedClassName('App\Models\UserAccount');   // "app.models.user-account"
StringHelper::fromDottedClassName('app.models.user-account'); // "App\Models\UserAccount"

StringHelper::autoEncodeToUtf8($legacyString);        // detects the current encoding and converts to UTF-8
StringHelper::decodeLegacyText($latin1String);        // collapses whitespace, strips  , decodes Latin-1 (deprecated: only for known non-UTF-8 input)

Need to replace only the first occurrence of a substring, or slugify a string for a URL? Use Laravel's own Str::replaceFirst() / Str::slug() directly — this package used to wrap both, but they're already exactly what you need with no difference in behavior.

ArrayHelper

CaueSantos\Support\Helpers\ArrayHelper — recursive array transforms, plucking, key renaming, and tree building.

use CaueSantos\Support\Helpers\ArrayHelper;

ArrayHelper::unsetRecursive(
    ['a' => 1, 'b' => ['c' => 2, 'd' => 'remove-me']],
    ['remove-me']
); // ['a' => 1, 'b' => ['c' => 2]]

ArrayHelper::removeEmpty([
    'name' => 'Ada', 'nickname' => '', 'age' => 0, 'tags' => [],
]); // ['name' => 'Ada', 'age' => 0] — 0/false are kept, blank values are stripped

ArrayHelper::deepMerge(
    ['settings' => ['theme' => 'dark', 'tags' => ['a']]],
    ['settings' => ['locale' => 'en', 'tags' => ['b']]],
); // ['settings' => ['theme' => 'dark', 'tags' => ['a', 'b'], 'locale' => 'en']]

ArrayHelper::diffRecursive(
    ['name' => 'Ada', 'address' => ['city' => 'Paris', 'zip' => 'E1']],
    ['name' => 'Ada', 'address' => ['city' => 'London', 'zip' => 'E1']],
); // ['address' => ['city' => 'Paris']] — only the changed leaf, not the whole nested array

ArrayHelper::toTree([
    ['id' => 1, 'parent_id' => null, 'name' => 'Root'],
    ['id' => 2, 'parent_id' => 1, 'name' => 'Child'],
]); // [['id' => 1, ..., 'children' => [['id' => 2, ..., 'children' => []]]]]

ArrayHelper::valuesRecursive(['a' => 1, 'b' => ['a' => 2, 'c' => 3]], 'a'); // [1, 2]

ArrayHelper::pluckRecursive(['id' => 1, 'child' => ['id' => 2]], 'id');    // [1, 2]

ArrayHelper::renameKeysNested(
    [['old' => 1, 'keep' => 'x']],
    ['old' => 'new']
); // [['new' => 1, 'keep' => 'x']]

ArrayHelper::convertKeysToCamel(['foo_bar' => 1, 'baz_qux' => 2]);
// ['fooBar' => 1, 'bazQux' => 2]

ArrayHelper::isListOfArrays([['id' => 1], ['id' => 2]]); // true — a sequential list containing at least one array
ArrayHelper::isListOfArrays(['id' => 1]);                // false

// Normalize request-style scalars: "true"/"false"/"123" -> real bool/int
ArrayHelper::normalizeScalarsRecursive(['active' => 'true', 'count' => '5']);
// ['active' => true, 'count' => 5]

// Join several attributes of ONE related record with " - "
ArrayHelper::joinRelationAttributes(
    ['user' => ['first_name' => 'Ada', 'last_name' => 'Lovelace']],
    ['user', 'first_name@last_name']
); // "Ada - Lovelace"

isListOfArrays() looks similar to Laravel's native Arr::isList() but checks something different: Arr::isList() only cares whether the keys are sequential, while isListOfArrays() additionally requires at least one element to itself be an array.

modifyRecursive(), mapRecursive(), buildPathArrays() and pluckGrouped() cover tree/menu-shaped data — see their docblocks in src/Helpers/ArrayHelper.php for the exact shape each one expects and returns (buildPathArrays() in particular returns one flattened list of every leaf's path segments, not one path-array per leaf — check its docblock before relying on the shape).

ObjectHelper

CaueSantos\Support\Helpers\ObjectHelper — deep array ⇄ stdClass conversion via a JSON round-trip.

use CaueSantos\Support\Helpers\ObjectHelper;

ObjectHelper::toArray((object) ['a' => 1, 'b' => (object) ['c' => 2]]);
// ['a' => 1, 'b' => ['c' => 2]]

ObjectHelper::toStdClass(['a' => 1, 'b' => ['c' => 2]]);
// stdClass { a: 1, b: stdClass { c: 2 } }

DateHelper

CaueSantos\Support\Helpers\DateHelper — date validation/formatting and minute-to-hour formatting.

use CaueSantos\Support\Helpers\DateHelper;

DateHelper::isValidDate('2024-01-31', 'Y-m-d'); // true
DateHelper::isValidDate('2024-02-30', 'Y-m-d'); // false (not a real date)

DateHelper::formatUkDate('2024-03-05');               // "05/03/2024"
DateHelper::formatUkDate('2024-03-05 14:30:00', true); // "05/03/2024 14:30h"

DateHelper::minutesToHours(90);  // "1:30"
DateHelper::minutesToHours(65);  // "1:05"
DateHelper::minutesToHours(120); // "2"

isValidDate() isn't the same as Carbon's own Carbon::hasFormat(): hasFormat() only checks that a string matches a format's shape — it actually returns true for Carbon::hasFormat('2024-02-30', 'Y-m-d'), an impossible date. isValidDate() round-trips the value back through the same format and rejects it if it doesn't match exactly, so it correctly returns false there.

For "now, in a given timezone", just use Laravel's own now('UTC') — no helper needed.

NumberHelper

CaueSantos\Support\Helpers\NumberHelper — byte formatting, memory usage, lightweight id generation, and clamping.

use CaueSantos\Support\Helpers\NumberHelper;

NumberHelper::formatBytes(1536);          // "1.5 KB"
NumberHelper::formatBytes(1024 * 1024);   // "1 MB"

NumberHelper::clamp(15, 0, 10); // 10 — constrain a value between a min and max

NumberHelper::memoryUsage(); // e.g. "2.34 megabytes" — current PHP memory usage

// A low-stakes, human-facing id — NOT guaranteed unique, don't use for
// primary keys or anything security-sensitive.
NumberHelper::almostUniqueId();

Laravel's own Number::fileSize() (Illuminate\Support\Number) looks similar but always pads to the given precision (Number::fileSize(1536, precision: 2) -> "1.50 KB"), where formatBytes() trims trailing zeros ("1.5 KB"). Pick whichever output style you want — they're genuinely different, not interchangeable.

MoneyHelper

CaueSantos\Support\Helpers\MoneyHelper — rounding-safe, cents-based money conversion and arithmetic. For currency formatting (symbols, locales), use Laravel's own Number::currency() instead — this class only covers what that doesn't: safe integer arithmetic.

use CaueSantos\Support\Helpers\MoneyHelper;

MoneyHelper::toCents(19.99);          // 1999
MoneyHelper::fromCents(1999);         // 19.99
MoneyHelper::formatFromCents(1999);   // "19.99"

// Split $10.00 three ways without losing or gaining a cent to float rounding
MoneyHelper::allocate(1000, 3); // [334, 333, 333] — sums back to exactly 1000

ValueHelper

CaueSantos\Support\Helpers\ValueHelper — emptiness checks and instance-of checks that read better than nested ternaries.

use CaueSantos\Support\Helpers\ValueHelper;

ValueHelper::firstValue(null, '', 'fallback', 'unused'); // "fallback"
ValueHelper::firstValue(null, '', 0, 'unused');          // 0 — Laravel's filled() treats 0 as filled

ValueHelper::valueOrNull('');   // null
ValueHelper::valueOrNull('x');  // "x"

ValueHelper::emptyMultiple(null, '', 0, []); // true
ValueHelper::emptyMultiple(null, 'x');       // false

ValueHelper::isInstanceOfAny($object, [Countable::class, Traversable::class]);
ValueHelper::isInstanceOfAll($object, [Countable::class, Traversable::class]);

ValidationHelper

CaueSantos\Support\Helpers\ValidationHelper — the one thing Laravel doesn't already give you a one-liner for: namespacing attributes for "each row of this array" validation rules.

use CaueSantos\Support\Helpers\ValidationHelper;

ValidationHelper::toRowValidationAttributes(['name' => 'Name', 'email' => 'Email'], 'rows');
// ['rows.*.name' => 'Name', 'rows.*.email' => 'Email']

To build a ValidationException from a plain attribute => message(s) array, just call Laravel's own \Illuminate\Validation\ValidationException::withMessages($errors) directly — it already accepts exactly that shape, no wrapper needed.

HtmlHelper

CaueSantos\Support\Helpers\HtmlHelper — small HTML-generation utilities.

use CaueSantos\Support\Helpers\HtmlHelper;

// Safe to drop straight into a Blade attribute: <div data-payload="{{ ... }}">
HtmlHelper::encodeForHtmlAttribute(['id' => 1, 'name' => 'Ada']);

// Array -> HTML attribute string, for contexts outside a Blade component's
// attribute bag (mailables, PDFs, plain-PHP string building)
HtmlHelper::attributes(['class' => 'btn', 'disabled' => true, 'title' => null]);
// 'class="btn" disabled' — true renders bare, false/null are omitted

// A quick Bootstrap-styled <table> from an array/Collection of rows
HtmlHelper::simpleTable(
    [['name' => 'Bob', 'age' => 30], ['name' => 'Sue', 'age' => 25]],
    [
        'name',                                    // dotted attribute path
        'age',
        'label' => fn ($value, $row) => strtoupper($row['name']), // computed column
    ]
);

FileHelper

CaueSantos\Support\Helpers\FileHelper — temp files, zip/unzip, CSV generation, MIME labels.

use CaueSantos\Support\Helpers\FileHelper;

// Written now, deleted automatically at the end of the request
$path = FileHelper::temporaryFile('report.csv', $csvContents);

FileHelper::zip(storage_path('archive.zip'), ['report.csv' => $path]);
FileHelper::unzip(storage_path('extracted'), storage_path('archive.zip'));

FileHelper::generateCsvString([['name', 'age'], ['Ada', 36]]); // "name,age\nAda,36\n"

FileHelper::mimeLabel('image/png');         // "PNG Image"
FileHelper::mimeLabel('application/x-foo'); // "Unknown File" (default fallback)

UrlHelper

CaueSantos\Support\Helpers\UrlHelper — external-vs-internal URL detection and open-redirect protection. Laravel doesn't ship anything for this out of the box.

use CaueSantos\Support\Helpers\UrlHelper;

UrlHelper::isExternal('/dashboard');                     // false — relative
UrlHelper::isExternal('https://example.com/dashboard');  // false — same host as config('app.url')
UrlHelper::isExternal('https://evil.com/dashboard');     // true

// Guard a `?redirect=` query param before redirecting a user to it
UrlHelper::isSafeRedirectTarget($request->query('redirect'));       // true only for a relative path or your own host
UrlHelper::isSafeRedirectTarget('//evil.com/path');                 // false — protocol-relative bypass
UrlHelper::isSafeRedirectTarget('https://partner.com', ['partner.com']); // true — explicit allow-list

EnumHelper

CaueSantos\Support\Helpers\EnumHelper — utilities for any PHP 8.1+ enum (backed or pure).

use CaueSantos\Support\Helpers\EnumHelper;

EnumHelper::values(Status::class); // ['open', 'closed'] (backing values), or case names for a pure enum
EnumHelper::names(Status::class);  // ['Open', 'Closed']

// value => label pairs for a <select>, defaulting to a "Title Case" label
EnumHelper::toSelectOptions(Status::class);
// ['open' => 'Open', 'closed' => 'Closed']

EnumHelper::toSelectOptions(Status::class, ['Open' => 'Currently open']);
// ['open' => 'Currently open', 'closed' => 'Closed']

See also InteractsWithEnum to add these as methods directly on the enum itself.

CollectionHelper

CaueSantos\Support\Helpers\CollectionHelper — building nested Laravel Collections that support both array and object-style access.

use CaueSantos\Support\Helpers\CollectionHelper;

$data = CollectionHelper::toDeepCollection([
    'name' => 'Ada',
    'address' => ['city' => 'London', 'zip' => 'E1'],
]);

$data->name;              // "Ada"
$data['name'];            // "Ada" — same value, array access
$data->address->city;     // "London" — nested arrays become nested Collections too
$data->toArray();         // ['name' => 'Ada', 'address' => ['city' => 'London', 'zip' => 'E1']]

EloquentHelper

CaueSantos\Support\Helpers\EloquentHelper — relation-loaded checks, model mapping, and closures for Eloquent's constrained eager loading.

use CaueSantos\Support\Helpers\EloquentHelper;

// Check a relation (or a dot-path of relations) is already loaded, without
// triggering a lazy load — safe to call speculatively.
EloquentHelper::modelHasRelations($post, 'author');
EloquentHelper::modelHasRelations($author, 'posts.comments');

// Build a fresh, unsaved model from a plain array
$mapper = EloquentHelper::mapToModel(Post::class);
$post = $mapper(['title' => 'Hello world']);

// The withXxx() methods return closures for constrained eager loading:
// `Model::with(['relation' => EloquentHelper::withXxx(...)])`. Each one
// normalizes a scalar-or-array-or-null input the way the underlying
// Eloquent method itself doesn't — for a plain where()/orderBy()/
// withTrashed() constraint, just write `fn ($q) => $q->where(...)`
// yourself, it's exactly as short.
Post::with(['author' => EloquentHelper::withoutGlobalScopes()])->get(); // removes every global scope
Post::with(['author' => EloquentHelper::withOnly(['name', 'email'])])->get();
Post::with(['comments' => EloquentHelper::without(['replies'])])->get();
Post::with(['comments' => EloquentHelper::withScope(['published', 'recent' => [30]])])->get();

SchemaHelper

CaueSantos\Support\Helpers\SchemaHelper — schema introspection, DDL helpers and SQL fragment building.

use CaueSantos\Support\Helpers\SchemaHelper;

SchemaHelper::wrapColumn('name', 'users');    // "`users`.`name`" (grammar-specific quoting)
SchemaHelper::unwrapColumn('`users`.`name`'); // "users.name"

SchemaHelper::buildAggregateExpression('amount', 'sum', 'total'); // "SUM(amount) as total"
SchemaHelper::buildAggregateExpression('id');                     // "COUNT(id)"

SchemaHelper::shortForeignKeyName('a_very_long_table_name', 'a_very_long_column'); // "fk_" + 24-char hash, always <= 64 chars

SchemaHelper::dropColumnIfExists('posts', 'legacy_column');
SchemaHelper::dropForeignKeyIfExists('posts', 'author_id');
SchemaHelper::dropForeignKeyAndColumnIfExists('posts', 'author_id');

dropColumnIfExists() is portable across every driver Laravel supports (MySQL, MariaDB, Postgres, SQLite, SQL Server). The foreign-key methods are portable on Laravel 11+ (via Schema::getForeignKeys()), with a MySQL/MariaDB-only fallback on older versions — note that SQLite's foreign keys have no constraint name at all, so they can be detected but not dropped there. canBeHistorical() stays MariaDB-only: system versioning has no equivalent in MySQL, Postgres or SQL Server.

SqlHelper

CaueSantos\Support\Helpers\SqlHelper — parsing and analyzing raw SQL strings. Requires greenlion/php-sql-parser (see Optional dependencies).

use CaueSantos\Support\Helpers\SqlHelper;

SqlHelper::getExpressionByAlias('SELECT COUNT(*) as total FROM posts', 'total'); // "COUNT(*)"

SqlHelper::containsMySQLFunction('CONCAT(first_name, last_name)'); // true
SqlHelper::containsMathOperators('price * 1.2');                  // true
SqlHelper::hasAggregation($query); // true if the query (string or Builder) contains SUM/COUNT/AVG/...

LaravelHelper

CaueSantos\Support\Helpers\LaravelHelper — small, one-off framework integrations that don't warrant their own class.

use CaueSantos\Support\Helpers\LaravelHelper;

// 409 response required by Inertia when redirecting from a PUT/PATCH/DELETE
// request. Requires inertiajs/inertia-laravel — see Optional dependencies.
return LaravelHelper::forceInertiaRedirect(route('posts.index'));

// Log a user in from a queue job without leaking a stale user between jobs
// on a long-running worker (Horizon).
LaravelHelper::queueSafeLogin($job->user_id);

// Every translation file for a locale, flattened into one array keyed by filename
LaravelHelper::loadLocaleTranslations('en');

CacheHelper

CaueSantos\Support\Helpers\CacheHelper — a cache-stampede-safe remember(). Requires a cache store that supports atomic locks (redis, memcached, database, dynamodb, or the array store).

use CaueSantos\Support\Helpers\CacheHelper;

// If the key is missing, only one process computes the callback while every
// other concurrent caller waits on a lock and then reads the fresh value —
// unlike Cache::remember(), where every concurrent miss runs the callback.
CacheHelper::rememberSafely('expensive-report', now()->addHour(), function () {
    return Report::buildExpensiveReport();
});

Looking for a "serve stale data while refreshing in the background" strategy instead of blocking? That's Laravel's own Cache::flexible() — a different trade-off, not a replacement for this.

LabelHelper

CaueSantos\Support\Helpers\LabelHelper — tiny display-label formatting.

use CaueSantos\Support\Helpers\LabelHelper;

LabelHelper::yesNo(true);  // "Yes"
LabelHelper::yesNo(0);     // "No"

LabelHelper::boolLabel($post->is_active, 'Active', 'Inactive');

LabelHelper::statusLabel($post->status, ['draft' => 'Draft', 'published' => 'Published']);
LabelHelper::statusLabel('archived', ['draft' => 'Draft'], default: 'Unknown'); // "Unknown"

SseHelper

CaueSantos\Support\Helpers\SseHelper — a minimal Server-Sent Events response helper.

use CaueSantos\Support\Helpers\SseHelper;

SseHelper::start(); // sends SSE headers and clears output buffers

foreach ($progressUpdates as $update) {
    SseHelper::send(['percent' => $update], event: 'progress');
}

SseHelper::end('done'); // sends a final "end" event and terminates the request

Object hydration

Two traits and one static class work together to fill a plain PHP object's typed properties from an array — casting scalars to their declared type, parsing dates into Carbon, resolving enums, building nested Collections, and recursively hydrating nested typed objects.

use CaueSantos\Support\IsHydratable;
use CaueSantos\Support\AutoJsonSerializable;
use Illuminate\Support\Carbon;

class Address
{
    use IsHydratable, AutoJsonSerializable;

    public string $city = '';
    public ?string $zip = null;
}

class Person
{
    use IsHydratable, AutoJsonSerializable;

    public string $name = '';
    public int $age = 0;
    public ?Carbon $joinedAt = null;
    public Address $address;
}

$person = Person::hydrate([
    'name' => 'Ada',
    'age' => '36',                              // cast to int
    'joinedAt' => '2024-01-01',                  // parsed into Carbon
    'address' => ['city' => 'London', 'zip' => 'E1'], // recursively hydrated
]);

$person->age;                  // 36 (int)
$person->joinedAt->isToday();  // false — it's a real Carbon instance
$person->address->city;        // "London"
  • IsHydratableuse this on any class to get ::hydrate($data) (new instance), ::hydrateNoConstructor($data) (skips your __construct(), useful when it has required arguments), and ->hydrateCurrent($data) (hydrate an already-built instance in place). Accepts an array, an ArrayAccess implementation, or a stdClass (e.g. straight from json_decode()).
  • AutoJsonSerializable — adds jsonSerialize(): array, reflecting over every non-static public/protected/private property (recursing into nested objects, unwrapping enum values). To have json_encode($person) call it automatically, also implements \JsonSerializable on the class; otherwise call $person->jsonSerialize() directly. Override serializeWith() to merge in computed keys that aren't backed by a real property.
  • ClassHydrator — the static engine both traits delegate to.
    • ClassHydrator::hydrate($existingInstance, $data) — hydrate an object you didn't build through IsHydratable.
    • ClassHydrator::hydrateMany(Person::class, $rows) — hydrate a list of arrays into a Collection of Person instances in one call.
    • ClassHydrator::dehydrate($person) — the reverse of hydration: a plain array back out of a hydrated object (enums unwrapped to their value, dates formatted as ISO-8601 strings, nested objects/Collections recursed into), without going through a JSON round-trip like AutoJsonSerializable does.
  • ClassPropertyHelper — the lower-level type-coercion primitives (toBoolean(), toInteger(), convertValue(), ...) ClassHydrator is built on. Most consumers won't need to call it directly.

Model traits (HasSlug, HasMetadata)

HasSlug auto-generates a slug column from another attribute when a model is created, appending -2, -3, etc. if it would otherwise collide:

use CaueSantos\Support\HasSlug;

class Post extends Model
{
    use HasSlug;

    // Optional overrides — these are the defaults:
    public function slugSourceField(): string { return 'name'; }
    public function slugField(): string { return 'slug'; }
    public function shouldUpdateSlugOnUpdate(): bool { return false; }
}

Post::create(['name' => 'My Cool Title'])->slug; // "my-cool-title"

HasMetadata gives a model a flexible, dot-notation-aware key-value store backed by a single JSON column (default: meta — cast it to array):

use CaueSantos\Support\HasMetadata;

class Post extends Model
{
    use HasMetadata;

    protected $casts = ['meta' => 'array'];
}

$post->setMeta('seo.title', 'Hello, world');
$post->getMeta('seo.title');        // "Hello, world"
$post->getMeta('seo.missing', 'x'); // "x" — default when missing
$post->hasMeta('seo.title');        // true
$post->forgetMeta('seo.title');

InteractsWithEnum

use on a backed or pure enum to add a default label() derived from the case name, plus options()/values()/names() static passthroughs to EnumHelper. Override label() on the enum itself for custom per-case labels.

use CaueSantos\Support\InteractsWithEnum;

enum Status: string
{
    use InteractsWithEnum;

    case Open = 'open';
    case Closed = 'closed';
}

Status::Open->label();  // "Open" (Str::headline() of the case name)
Status::options();      // ['open' => 'Open', 'closed' => 'Closed']

Makeable objects (IsMakeable, Component)

IsMakeable is a one-method trait: use it on any class to get a ::make(...$arguments) static constructor that just forwards to new static(...$arguments) — handy for a fluent, static-friendly construction style.

use CaueSantos\Support\IsMakeable;

class Money
{
    use IsMakeable;

    public function __construct(public int $cents, public string $currency = 'USD') {}
}

Money::make(500, 'GBP'); // same as `new Money(500, 'GBP')`

Component is a tiny, ready-made value object built on IsMakeable for describing a named UI component with its data — e.g. handing a Blade/Inertia component name and props around as a single value:

use CaueSantos\Support\Component;

$component = Component::make('UserCard', ['id' => 1]);
$component->name; // "UserCard"
$component->data; // ['id' => 1]

Runtime migration generation

RuntimeMigration builds a real Laravel migration file (not a migration run in-process) from a Blueprint closure, so you can generate migrations programmatically — e.g. from a schema-builder UI, an artisan command, or a code generator.

use CaueSantos\Support\RuntimeMigration;
use Illuminate\Database\Schema\Blueprint;

$migration = RuntimeMigration::make('posts', function (Blueprint $table) {
    $table->string('title');
    $table->text('body')->nullable();
});

$path = $migration->create(database_path('migrations'));
// writes a real timestamped `..._create_posts_table.php` migration file and
// returns its path

$migration
    ->afterMigrationCreated(fn (string $file, string $table) => Log::info("Generated {$file} for {$table}"))
    ->create(database_path('migrations'));

// Generate an "update" migration for an existing table instead — its down()
// reverses the same add-column statements (MySQL/MariaDB syntax).
$migration->update(database_path('migrations'));

If the target directory doesn't exist, or the file can't be written, create()/update() throw CaueSantos\Support\Exceptions\MigrationGenerationException.

Two smaller pieces support this:

  • Stub — a minimal {{ placeholder }} template renderer used to fill in the bundled stubs/migration.create.stub and stubs/migration.update.stub templates. Stub::create($path, ['table' => 'posts'])->render().
  • HistoricalDataMigration — wraps MariaDB's system-versioning (ADD SYSTEM VERSIONING) DDL so an "update" migration generated by RuntimeMigration can safely alter a historical/versioned table. MariaDB only.

Optional dependencies

Two methods need a package this library doesn't force on every consumer — both are declared under Composer's suggest, not require, so install them only if you actually call the method that needs them:

Package Needed for
greenlion/php-sql-parser SqlHelper::getExpressionByAlias()
inertiajs/inertia-laravel LaravelHelper::forceInertiaRedirect()

Testing & code quality

composer install
vendor/bin/phpunit       # test suite
vendor/bin/pint --test   # code style (Laravel preset)
vendor/bin/phpstan analyse # static analysis (Larastan, level 5)

All three run in CI on every push and pull request, across a PHP 8.2–8.4 matrix (see .github/workflows/).

Security

If you discover any security related issues, please email instead of using the issue tracker.

License

MIT. See LICENSE.