damianulan / laravel-lucent
Lucent is a laravel extension package. It contains useful traits, helpers, macros, contracts and extensions such as ClassDiscovery and Services.
Requires
- php: ^8.3
- ext-json: *
- illuminate/support: ^9.0|^10.0|^11.0|^12.0
- mews/purifier: ^3.4
Requires (Dev)
- laravel/pint: ^1.0
- mockery/mockery: ^1.5
- phpunit/phpunit: ^11.0|^12.0
Suggests
- laravel/framework: ^11.0
README
Laravel Lucent is a utility package for Laravel applications that bundles a small set of reusable primitives:
- Transaction-oriented service classes
- Composer class discovery via Magellan scopes
- Eloquent traits for UUIDs, access scopes, cascade deletes, pruning, and model state helpers
- HTML sanitizing and trait inspection helpers
- String and currency lookup utilities
- A few console generators and maintenance commands
The package is intentionally lightweight. Most features are opt-in and can be used independently.
Requirements
- PHP
^8.3 illuminate/support^9.0|^10.0|^11.0|^12.0mews/purifier^3.4
Installation
Install the package with Composer:
composer require damianulan/laravel-lucent
Laravel package discovery registers the service provider automatically.
If you want the package config, translations, and stubs in your application, publish them:
php artisan vendor:publish --tag=lucent
You can also publish individual groups:
php artisan vendor:publish --tag=lucent-config php artisan vendor:publish --tag=lucent-langs
What The Package Includes
Services
Lucent\Services\Service is a base class for application services that:
- accepts named boot parameters
- runs
handle()inside a database transaction - supports optional authorization and validation
- collects runtime or validation errors
- exposes the original input and the returned result
- provides a small cache helper via
remember()
Generate a service class:
php artisan make:service CreateOrUpdateCampaign
Example service:
<?php namespace App\Services; use App\Models\Campaign; use Lucent\Services\Service; class CreateOrUpdateCampaign extends Service { protected function authorize(): bool { return $this->request()->user()?->can('campaigns.manage') ?? false; } protected function rules(): array { return [ 'name' => ['required', 'string', 'max:255'], ]; } protected function handle(): Campaign|false { if (! $this->validate($this->request()->all())) { return false; } $campaign = $this->campaign ?? new Campaign(); $campaign->fill($this->request()->only(['name', 'description'])); $campaign->save(); return $campaign; } }
Execute it from a controller or action:
$service = CreateOrUpdateCampaign::boot( request: $request, campaign: $campaign, )->execute(); if (! $service->passed()) { return back()->withErrors($service->getErrors()); } $savedCampaign = $service->getResult();
Useful methods:
boot(...$props): instantiate the service with named argumentsexecute(): run authorization, thenhandle()insideDB::transaction()add(...$props): append more named data after bootingrequest(): access the bound request or an empty request objectgetOriginal(): get the original booted input as a collectiongetResult(): get the value returned byhandle()getErrors(): get collected validation/runtime messageshasErrors(): check whether any errors were collectedtoArray()/toJson(): serialize the original input payload
Notes:
execute()only marks the service as passed whenhandle()returns a truthy value.- If authorization fails or an exception is thrown, the exception is reported and the message is added to the error bag.
Magellan Scopes
Lucent\Support\Magellan\MagellanScope lets you discover classes from Composer's class map and filter them with reflection-based rules.
This is useful when you want to locate application classes or approved vendor classes dynamically, for example form builders, policies, handlers, or plugin-like classes.
Generate a scope:
php artisan make:magellan AdminFormScope
Inline usage:
use App\Forms\BaseForm; use Lucent\Support\Magellan\MagellanScope; $forms = MagellanScope::blacklist([ 'App\\Console\\', 'App\\Providers\\', ]) ->filter(fn (\ReflectionClass $class) => $class->isSubclassOf(BaseForm::class)) ->get();
Custom scope class:
namespace App\Support\Magellan; use Lucent\Support\Magellan\MagellanScope; use Lucent\Support\Magellan\Workshop\ScopeUsesCache; class PolicyScope extends MagellanScope implements ScopeUsesCache { protected function scope(\ReflectionClass $class): bool { return str_ends_with($class->getName(), 'Policy'); } public function ttl(): int { return 3600; } }
Then use it:
$policies = PolicyScope::get();
Important behavior:
- The scope reads from
vendor/composer/autoload_classmap.php - Application classes are included by default
- Vendor classes are excluded by default unless allowed in
config/lucent.phpundermagellan.vendor_include - Implement
ScopeUsesCacheto cache the collected class list
Helpers available on a filled scope:
get()fill()toArray()toJson()count()
Eloquent Traits
Accessible
Adds a local scope named checkAccess() which applies a custom Eloquent scope stored on the model.
use App\Models\Scopes\UserScope; use Illuminate\Database\Eloquent\Model; use Lucent\Support\Traits\Accessible; class User extends Model { use Accessible; protected string $accessScope = UserScope::class; }
$visibleUsers = User::query()->checkAccess()->get();
The configured scope class must extend Illuminate\Database\Eloquent\Scope.
UUID
Adds UUID primary key support.
use Illuminate\Database\Eloquent\Model; use Lucent\Support\Traits\UUID; class Order extends Model { use UUID; }
Migration example:
Schema::create('orders', function (Blueprint $table) { $table->uuid('id')->primary(); });
The trait disables incrementing and fills the primary key with Str::uuid() on create.
HasUniqueUuid
Adds a unique UUID column without replacing the model primary key.
use Illuminate\Database\Eloquent\Model; use Lucent\Support\Traits\HasUniqueUuid; class Order extends Model { use HasUniqueUuid; }
Migration example:
Schema::create('orders', function (Blueprint $table) { $table->id(); $table->uuid('uuid')->unique(); });
Lookup methods:
$order = Order::findByUuid($uuid); $uuid = $order->getUuidKey();
If your UUID column has a different name, override:
public static function getUuidKeyName(): string { return 'public_id'; }
VirginModel
Adds convenience helpers around common boolean active and draft flags.
use Illuminate\Database\Eloquent\Model; use Lucent\Support\Traits\VirginModel; class Post extends Model { use VirginModel; protected $fillable = [ 'title', 'active', 'draft', ]; }
Available helpers:
Post::getAll(); Post::allActive(); Post::allInactive(); Post::allPublished(); Post::allDrafts(); Post::query()->active()->get(); Post::query()->inactive()->get(); Post::query()->published()->get(); Post::query()->drafted()->get(); $post->empty(); $post->notEmpty();
active() and drafted() scopes only apply when the corresponding fields are present in $fillable.
CascadeDeletes
Deletes related models when the parent model is deleted. This works through the deleted model event, so it does not run for mass deletes that bypass model events.
use Illuminate\Database\Eloquent\Model; use Lucent\Support\Traits\CascadeDeletes; class User extends Model { use CascadeDeletes; protected array $cascadeDelete = ['profile', 'posts']; }
If cascadeDelete is omitted and lucent.models.auto_cascade_deletes is enabled, Lucent tries to detect deletable relation methods automatically based on the configured relation return types.
You can also block specific relations:
protected array $donotCascadeDelete = ['auditLogs'];
Configuration:
'models' => [ 'auto_cascade_deletes' => true, 'cascade_delete_relation_types' => [ Illuminate\Database\Eloquent\Relations\MorphMany::class, Illuminate\Database\Eloquent\Relations\MorphToMany::class, Illuminate\Database\Eloquent\Relations\BelongsToMany::class, Illuminate\Database\Eloquent\Relations\HasMany::class, Illuminate\Database\Eloquent\Relations\HasOne::class, ], ]
SoftDeletesPrunable
Provides a prunableSoftDeletes() scope for models using Laravel's SoftDeletes.
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; use Lucent\Support\Traits\SoftDeletesPrunable; class Archive extends Model { use SoftDeletes; use SoftDeletesPrunable; }
This scope is used by the pruning command described below.
Helpers
clean_html()
Sanitizes rich HTML input using the package's dedicated Purifier preset.
$safe = clean_html($request->input('body'));
Examples:
clean_html('<script>alert(1)</script>'); // '' clean_html('<p class="text-center"><strong>Hello</strong></p>');
Lucent merges an extra lucent_config entry into purifier.settings at boot time. The defaults are defined in config/lucent.php and allow common rich-text tags, safe links, some formatting classes, and selected inline CSS properties.
class_uses_trait()
Checks whether a class uses a trait anywhere in its inheritance tree.
use App\Models\User; use Lucent\Support\Traits\Accessible; if (class_uses_trait(Accessible::class, User::class)) { // ... }
Signature:
class_uses_trait(string $traitClass, string $targetClass): bool
Support Utilities
Lucent\Support\Str\Alphabet
Utility for working with Latin letters, including accented UTF-8 variants.
use Lucent\Support\Str\Alphabet; Alphabet::getAlphabetPosition('A'); // 1 Alphabet::getAlphabetPosition('É'); // 5 Alphabet::getAlphabetPosition('Ż'); // 26 Alphabet::getAlphabetPosition('ß'); // null
The class normalizes accented characters to their ASCII base where possible before calculating the alphabet position.
Lucent\Support\Str\Currencies\CurrencyLib
Provides an in-memory ISO 4217 currency dataset.
use Lucent\Support\Str\Currencies\CurrencyLib; $currencies = new CurrencyLib(); $eur = $currencies->getByAlpha3('EUR'); $usd = $currencies->getByCode('840'); $all = $currencies->getAll();
Returned items use this shape:
[
'name' => 'Euro',
'alpha3' => 'EUR',
'numeric' => '978',
'exp' => 2,
'country' => 'EU',
]
Methods:
getByCode(string $code)getByAlpha3(string $alpha3)getByNumeric(string $numeric)getAll()
getByAlpha3() and getByNumeric() validate input format before lookup and throw an exception on invalid values.
Lucent\Console\Git
Structured helper around common git queries and release-oriented commands:
use Lucent\Console\Git; $branch = Git::head(); $tags = Git::getTags(); $latest = Git::getLatestTagName();
For richer inspection, build a repository-scoped instance:
$git = Git::repository(base_path()) ->queue(['git', 'status', '--short']) ->queue(['git', 'rev-parse', '--abbrev-ref', 'HEAD']) ->run(); $result = $git->lastResult();
It also exposes:
checkoutRelease(string $tag)checkoutLatestRelease()
Each executed command is captured as a GitResult object with the command, working directory, exit code, output, error output, and inferred caller metadata.
Lucent\Support\Trace
Captures and inspects the current backtrace:
use Lucent\Support\Trace; $trace = Trace::boot();
Useful helpers:
$caller = $trace->caller(); $steps = $trace->steps(oldestFirst: true, withSignature: true); $appFrames = $trace->onlyApplicationFrames()->withoutVendorFrames(); $details = $trace->details();
You can also build a trace from an exception:
try { // ... } catch (Throwable $exception) { $trace = Trace::fromThrowable($exception); }
Reflection is used internally to enrich frames with method signatures, namespaces, and callable metadata, making the tool suitable for debugging chained service calls, controller pipelines, and vendor-to-app handoffs.
Artisan Commands
make:service
Creates a service class in App\Services.
php artisan make:service PublishArticle
make:magellan
Creates a Magellan scope in App\Support\Magellan.
php artisan make:magellan FormScope
model:prune-soft-deletes
Permanently deletes soft-deleted records for models that:
- live under
App\Models - extend
Illuminate\Database\Eloquent\Model - use Laravel's
SoftDeletestrait - use
Lucent\Support\Traits\SoftDeletesPrunable
Run it manually:
php artisan model:prune-soft-deletes
Schedule it:
use Illuminate\Support\Facades\Schedule; Schedule::command('model:prune-soft-deletes')->daily();
Configure the age threshold in config/lucent.php or through environment:
PRUNE_SOFT_DELETES_DAYS=365
Configuration
Published config file: config/lucent.php
Main options:
return [ 'models' => [ 'prune_soft_deletes_days' => env('PRUNE_SOFT_DELETES_DAYS', 365), 'auto_cascade_deletes' => true, 'cascade_delete_relation_types' => [ Illuminate\Database\Eloquent\Relations\MorphMany::class, Illuminate\Database\Eloquent\Relations\MorphToMany::class, Illuminate\Database\Eloquent\Relations\BelongsToMany::class, Illuminate\Database\Eloquent\Relations\HasMany::class, Illuminate\Database\Eloquent\Relations\HasOne::class, ], ], 'mews_purifier_setting' => [ // custom purifier preset used by clean_html() ], 'magellan' => [ 'vendor_include' => [ 'spatie/', ], ], ];
Use magellan.vendor_include when a scope should inspect specific vendor namespaces from the Composer class map.
Typical Use Cases
- Wrap multi-step create/update flows in a dedicated service class
- Apply model-specific access constraints through reusable Eloquent scopes
- Add UUID primary keys or public UUID identifiers to Eloquent models
- Automatically cascade deletes through selected relations
- Sanitize rich text input before persisting or rendering it
- Discover application classes dynamically using reflection and Composer metadata
- Prune stale soft-deleted records on a schedule
Caveats
- Services are considered successful only when
handle()returns a truthy value. - Cascade delete logic relies on model events and will not run for mass delete queries that skip events.
- Magellan depends on Composer's generated class map. If class discovery looks stale, refresh autoload metadata with
composer dump-autoload. - The Lucent pipeline layer is deprecated.
License
MIT. See LICENSE.
Contact
Questions and contributions: damian.ulan@protonmail.com