laravel-support maintained by ca-santos
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
- Installation
- How this package is organized
- Helpers
- Object hydration
- Model traits (
HasSlug,HasMetadata) InteractsWithEnum- Makeable objects (
IsMakeable,Component) - Runtime migration generation
- Optional dependencies
- Testing & code quality
- Security
- License
Requirements
- PHP 8.2+
illuminate/support9.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 nativeArr::isList()but checks something different:Arr::isList()only cares whether the keys are sequential, whileisListOfArrays()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 ownCarbon::hasFormat():hasFormat()only checks that a string matches a format's shape — it actually returnstrueforCarbon::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 returnsfalsethere.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"), whereformatBytes()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
ValidationExceptionfrom a plainattribute => 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"
IsHydratable—usethis 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, anArrayAccessimplementation, or astdClass(e.g. straight fromjson_decode()).AutoJsonSerializable— addsjsonSerialize(): array, reflecting over every non-static public/protected/private property (recursing into nested objects, unwrapping enum values). To havejson_encode($person)call it automatically, alsoimplements \JsonSerializableon the class; otherwise call$person->jsonSerialize()directly. OverrideserializeWith()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 throughIsHydratable.ClassHydrator::hydrateMany(Person::class, $rows)— hydrate a list of arrays into aCollectionofPersoninstances 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 likeAutoJsonSerializabledoes.
ClassPropertyHelper— the lower-level type-coercion primitives (toBoolean(),toInteger(),convertValue(), ...)ClassHydratoris 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 bundledstubs/migration.create.stubandstubs/migration.update.stubtemplates.Stub::create($path, ['table' => 'posts'])->render().HistoricalDataMigration— wraps MariaDB's system-versioning (ADD SYSTEM VERSIONING) DDL so an "update" migration generated byRuntimeMigrationcan 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.