laravel-opinionated-essentials maintained by jand3v
Laravel Opinionated Essentials
Opinionated essentials for Laravel applications. Applies sensible framework defaults automatically and provides generators for common patterns.
Installation
composer require jand3v/laravel-opinionated-essentials
The service provider is auto-discovered — no manual registration required.
Configuration
Publish the config file to adjust or disable individual defaults:
php artisan vendor:publish --tag=opinionated-essentials-config
This creates config/opinionated-essentials.php in your application. Every default can be toggled via environment variables.
Opinionated Defaults
The following behaviors are applied automatically when the package is installed:
| Behavior | Config key | Env variable | Default |
|---|---|---|---|
| Eloquent lazy-loading prevention (non-production) | models.prevent_lazy_loading |
OPINIONATED_ESSENTIALS_PREVENT_LAZY_LOADING |
true |
| Carbon immutable dates | dates.immutable |
OPINIONATED_ESSENTIALS_IMMUTABLE_DATES |
true |
| Secure password rules in production | passwords.secure |
OPINIONATED_ESSENTIALS_SECURE_PASSWORDS |
true |
| Prohibit destructive DB commands in production | database.prevent-destruction |
OPINIONATED_ESSENTIALS_PREVENT_DB_DESTRUCTION |
true |
To disable a default, set the env variable to false or update config/opinionated-essentials.php.
Lazy Loading Prevention
Outside of production, accessing an unloaded Eloquent relationship throws a LazyLoadingViolationException, forcing eager loading and preventing N+1 queries:
// ✗ Throws LazyLoadingViolationException outside production
$post->comments;
// ✓ Always safe
Post::with('comments')->get();
Immutable Dates
All date instances returned by Eloquent are CarbonImmutable, preventing accidental mutation:
$user->created_at->addDay(); // returns a new instance, original unchanged
Secure Passwords (production only)
In production, Password::defaults() requires a minimum of 12 characters, mixed case, letters, numbers, symbols, and checks the password against the HaveIBeenPwned database:
// Use the default password rule in your form requests
'password' => ['required', Password::default()],
In non-production environments Password::default() returns null (no extra constraints), making registration easy during development.
Prohibit Destructive Commands (production only)
DB::prohibitDestructiveCommands(true) is called in production, preventing migrate:fresh, migrate:reset, and db:wipe from running accidentally.
Generators
make:action
Creates a new Action class in app/Actions:
php artisan make:action PlaceOrder
Generates app/Actions/PlaceOrderAction.php:
<?php
declare(strict_types=1);
namespace App\Actions;
final readonly class PlaceOrderAction
{
public function __construct() {}
public function execute(): void
{
//
}
}
The
Actionsuffix is added automatically. PassingPlaceOrderActionproduces the same result — no double suffix.
--dto option
Pass --dto to generate an accompanying DTO and wire it into execute():
php artisan make:action PlaceOrder --dto
Generates both app/Actions/PlaceOrderAction.php and app/DTOs/PlaceOrderData.php:
// app/Actions/PlaceOrderAction.php
final readonly class PlaceOrderAction
{
public function __construct() {}
public function execute(PlaceOrderData $data): void
{
//
}
}
// app/DTOs/PlaceOrderData.php
final readonly class PlaceOrderData
{
use MapsFromArray;
public function __construct(
// Add your properties here
) {}
}
make:dto
Creates a standalone DTO in app/DTOs without an Action:
php artisan make:dto PlaceOrder
Generates app/DTOs/PlaceOrderData.php (same structure as above). The Data suffix is added automatically — passing PlaceOrderData produces the same result.
DTOs and MapsFromArray
Every generated DTO uses the MapsFromArray trait, which maps an array (e.g. from $request->validated()) to constructor properties.
Defining a DTO
use Jand3v\OpinionatedEssentials\Dto\Attributes\MapFrom;
use Jand3v\OpinionatedEssentials\Dto\Concerns\MapsFromArray;
final readonly class PlaceOrderData
{
use MapsFromArray;
public function __construct(
public int $customerId,
public string $customerName, // mapped from 'customer_name'
#[MapFrom('order_total')]
public int $totalAmount, // mapped from 'order_total'
public ?string $note = null, // optional, defaults to null
) {}
}
Creating instances
From a controller with a validated form request:
public function store(Request $request): Response
{
$data = PlaceOrderData::from($request->validated());
// $data->customerId, $data->customerName, $data->totalAmount
}
Or from any array:
$data = PlaceOrderData::from([
'customer_id' => 42,
'customer_name' => 'Ada Lovelace',
'order_total' => 9900,
]);
Mapping resolution order
For each constructor parameter, the trait resolves the value in this order:
#[MapFrom('key')]— explicit array key (highest priority).- Exact camelCase name — e.g.
customerNamematches array keycustomerName. - snake_case fallback — e.g.
customerNamealso matchescustomer_name. - Default value or nullable — uses the parameter's default, or
nullif nullable. InvalidArgumentException— thrown when a required key is missing.
Testing
composer install
vendor/bin/pest
License
The MIT License (MIT). See LICENSE for details.