Lithium Hosting companion package for Laravel apps.
Find a file
Troy Siedsma 1d819c78f3 Modernize: PHP 8.2, PSR-4, drop dead deps, type hints, code cleanup
composer.json
  - PHP requirement: >=5.4.0 -> ^8.2 (matches the LiBilling stack)
  - illuminate/support: >=5.0 -> ^11.0 || ^12.0
  - illuminate/database: added (we touch Eloquent::class_uses_recursive
    + Model::query() directly)
  - guzzlehttp/guzzle dropped (never imported anywhere)
  - PSR-0 -> PSR-4 (PSR-0 is deprecated; PSR-4 is the modern standard)
  - minimum-stability: dev removed (default stable is correct)
  - description rewritten to actually describe what the package does
  - keywords expanded for discoverability

src/ layout
  - Flattened: src/LithiumHosting/LaravelCompanion/Traits/* moved to
    src/Traits/* (and similarly for Utilities/Contracts/Abstracts).
    Cuts 3 levels of unused nesting; matches PSR-4 convention.

HasLongIdsTrait
  - Added declare(strict_types=1) + return types
  - Dropped the commented-out alternative creating() block at the
    bottom; was dead code drift
  - SoftDeletes detection now uses class_uses_recursive() instead
    of class_uses(), so models that compose SoftDeletes through a
    base class or trait-of-traits still get the soft-deleted
    uniqueness check
  - getColumnName / getColumnLength / getCharset / getSeparator
    renamed to getLongIdColumnName / etc. to match the static-prop
    naming + cut accidental collisions with model-level methods of
    the same name. Functional behavior unchanged.

RandomStringGenerator
  - strict_types + full type hints on every method
  - generate() no longer infinitely recurses on impossible
    constraints (length 2 + all-classes required + 3+ classes
    enabled); throws LogicException after 50 attempts so the
    operator sees the bad config instead of a stack-overflow crash
  - call_user_func_array("{$this->model}::where", ...) replaced
    with direct $this->model::query()->where() (clearer, statically
    analyzable, faster)
  - setBannedWords() added so consumers can extend the filter or
    disable it for trusted internal codes without forking
  - getChars() now throws InvalidArgumentException on empty
    charset (used to silently produce empty strings)
  - setSeparator() validates the array shape before storing
    (previously accepted any 2-element input including non-string
    separators)

Observer / ModelObserver
  - Added strict_types + Model type hints on every callback
  - Observer now `implements ModelObserver` (was unrelated)
  - setSlug uses Illuminate\Support\Str::slug() instead of the
    long-deprecated global str_slug() helper

README rewritten with real usage docs, customization table, and the
package's actual purpose (the prior README was a stale copy-paste
of the GeoIP package title).

libilling_new sanity check: AccountRecoveryStaffEmailUrlTest still
passes (touches a model with HasLongIdsTrait); composer dump-autoload
clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 15:49:15 +00:00
src Modernize: PHP 8.2, PSR-4, drop dead deps, type hints, code cleanup 2026-05-22 15:49:15 +00:00
.gitignore Re-platform onto LithiumHosting/laravel-companion Forgejo origin 2026-05-22 13:14:19 +00:00
composer.json Modernize: PHP 8.2, PSR-4, drop dead deps, type hints, code cleanup 2026-05-22 15:49:15 +00:00
LICENSE Re-platform onto LithiumHosting/laravel-companion Forgejo origin 2026-05-22 13:14:19 +00:00
LICENSE.md Standardize composer.json + README + LICENSE 2026-05-22 15:03:45 +00:00
README.md Modernize: PHP 8.2, PSR-4, drop dead deps, type hints, code cleanup 2026-05-22 15:49:15 +00:00

Laravel Companion

A small Laravel utility kit that ships two things you reach for over and over again on every model-heavy project:

  • HasLongIdsTrait — auto-populates a stable, URL-safe identifier column on every Eloquent model that uses the trait. Use it as the public route key so your URLs read /orders/g4n7-b2k instead of /orders/123 and so consumers can't enumerate your tables by incrementing an integer.
  • RandomStringGenerator — the fluent string builder under the trait. Use it standalone for any other "give me a unique short code" need (support PINs, voucher codes, registrar registration keys, etc.).

Plus a tiny Observer / ModelObserver pair that pre-builds the standard Eloquent lifecycle hooks if you'd rather subclass than copy them every time.

Installation

composer require lithiumhosting/laravel-companion

No service provider registration; the trait is opt-in per model, the generator is new-able anywhere.

HasLongIdsTrait — public IDs without enumeration

Add a long_id column to your table:

Schema::table('orders', function (Blueprint $table) {
    $table->string('long_id', 6)->unique()->after('id');
});

Use the trait on your model:

use LithiumHosting\LaravelCompanion\Traits\HasLongIdsTrait;

class Order extends Model
{
    use HasLongIdsTrait;
}

On every creating event the trait generates a unique value into the column. The check honors SoftDeletes if the model uses it, so a soft-deleted g4n7-b2k won't be reused while it's still recoverable.

If you want the column to be your public route key:

public function getRouteKeyName(): string
{
    return 'long_id';
}

Customization

Set any of these protected static properties on your model to override the defaults:

Property Default Effect
$longIdColumnName 'long_id' Use a different column name (number, slug, reference, etc.)
$longIdColumnLength 6 Generated string length
$longIdCharset 'lower|numbers' Pipe-delimited combo: lower, upper, numbers, special
$longIdSeparator [] [position, char] — e.g. [3, '-'] produces abc-def

Example for an order-number-style column:

class Order extends Model
{
    use HasLongIdsTrait;

    protected static $longIdColumnName = 'number';
    protected static $longIdColumnLength = 8;
    protected static $longIdCharset = 'upper|numbers';
    protected static $longIdSeparator = [4, '-'];   // "AB12-CD34"
}

RandomStringGenerator — standalone

Inject it via app(RandomStringGenerator::class) or just new RandomStringGenerator() and chain:

use LithiumHosting\LaravelCompanion\Utilities\RandomStringGenerator;

$pin = (new RandomStringGenerator())
    ->setLength(6)
    ->setChars('numbers')
    ->setModel(UserSupportPin::class)
    ->setColumn('pin')
    ->generate();

Available builder methods:

Method Purpose
setLength(int) Output length, default 6
setChars(string $type, string $special = '') Charset combo (lower|upper|numbers|special) + optional explicit special chars
setSeparator([position, char]) Insert a separator every N chars
setPrefix(string) Prepend a literal prefix (e.g. 'SUP-')
setModel(string) Eloquent model class to check for uniqueness against
setColumn(string) Column on that model to dedupe against
requireAllClasses(bool) When true, every charset in setChars() MUST appear in the output at least once
generate(bool $withTrashed = false) Produce the string, dedupe against the model + column. Pass true to also check soft-deleted rows.
getRandomString() Produce a string without DB lookup (handy in seeders / tests)

The generator also runs every candidate through a banned-words filter so generated codes don't accidentally read as profanity, especially relevant when handing customers a code to read back on a phone call.

Observer + ModelObserver

Optional. If you write Eloquent observers and want a default no-op base class with every lifecycle hook (creating, created, updating, ..., restoring, restored) pre-stubbed, extend Observer or implement ModelObserver. Includes a setSlug($slug, $model, $slugColumn = 'slug') helper that uses Str::slug() and deduplicates within the model.

License

This package, laravel-companion is licensed under The MIT License (MIT). Please see License File for more information.

Is it any good?

Yes.

When people first hear about a new product, they frequently ask if it is any good. A Hacker News user remarked:

Note to self: Starting immediately, all raganwald projects will have a "Is it any good?" section in the readme, and the answer shall be "yes.".