laravel-utilities maintained by osoobe
Laravel Utilities
A collection of Eloquent model traits, static helper classes, migration Blueprint macros, a generic AJAX data endpoint, and a base Artisan command — designed to eliminate boilerplate across Laravel applications.
Install
composer require osoobe/laravel-utilities
The service provider is auto-discovered. No manual registration needed.
Table of Contents
- Migration Blueprint Macros
- Eloquent Model Traits
- Helper Classes
- Generic AJAX Resource Endpoint
- Custom Validators
- Base Artisan Command
- Use Cases
- Testing
Migration Blueprint Macros
The package registers Blueprint macros that add common column groups with a single call.
Location columns
Schema::create('businesses', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->location(); // country, state, city, street_address, zip_code
$table->addLocationIndex(); // adds individual indexes on each location column
$table->timestamps();
});
// Rolling back
Schema::table('businesses', function (Blueprint $table) {
$table->dropLocationIndex();
$table->dropLocation();
});
Coordinate columns
Schema::create('properties', function (Blueprint $table) {
$table->id();
$table->coordinates(); // latitude (decimal 16,13), longitude (decimal 16,13)
$table->timestamps();
});
Schema::table('properties', function (Blueprint $table) {
$table->dropCoordinates();
});
Userstamp columns
Tracks which user created and last edited a record using polymorphic morphs, supporting multi-model auth setups.
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->userstamp(); // creator_id, creator_type, editor_id, editor_type
$table->timestamps();
});
Schema::table('posts', function (Blueprint $table) {
$table->dropUserstamp();
});
Active status column
Schema::create('products', function (Blueprint $table) {
$table->id();
$table->isActive(); // is_active tinyint, nullable, default 1
$table->timestamps();
});
Eloquent Model Traits
Active — Active/inactive record scopes
Requires an is_active column (added via $table->isActive()).
use Osoobe\Utilities\Traits\Active;
class Product extends Model
{
use Active;
}
// Usage
Product::active()->get(); // WHERE is_active = 1
Product::notActive()->get(); // WHERE is_active != 1
Product::hidden()->get(); // WHERE hidden = 1
HasVerified — Verified record scopes
Requires a verified column.
use Osoobe\Utilities\Traits\HasVerified;
class Listing extends Model
{
use HasVerified;
}
Listing::verified()->get();
Listing::notVerified()->get();
IsDefault — Default record scopes
Requires an is_default column.
use Osoobe\Utilities\Traits\IsDefault;
class PaymentMethod extends Model
{
use IsDefault;
}
PaymentMethod::isDefault()->first();
PaymentMethod::notDefault()->get();
Sorted — Sort order scope
Requires a sort_order column.
use Osoobe\Utilities\Traits\Sorted;
class MenuItem extends Model
{
use Sorted;
}
MenuItem::sorted()->get(); // ORDER BY sort_order ASC
HasSlug — Slug-based lookup
use Osoobe\Utilities\Traits\HasSlug;
class Article extends Model
{
use HasSlug;
}
// Finds by slug OR id — useful for route model binding
$article = Article::findBySlugOrFail('my-article-title');
HasEmail — Email verification scopes
use Osoobe\Utilities\Traits\HasEmail;
class User extends Model
{
use HasEmail;
}
User::emailVerified()->get();
User::emailNotVerified()->get();
User::email('user@example.com')->first();
// Users who verified their email within the last 7 days
User::emailVerifiedSince(7)->get();
// Check on an instance
if ($user->isEmailVerified()) { ... }
Userstamp — Auto-fill creator/editor on save
Requires userstamp() Blueprint macro columns. Automatically sets creator_* on create and editor_* on update using Auth::user().
use Osoobe\Utilities\Traits\Userstamp;
class Invoice extends Model
{
use Userstamp;
}
// No manual work needed — creator and editor are set automatically on save.
$invoice = Invoice::create(['amount' => 500]);
echo $invoice->creator_type; // "App\Models\User"
echo $invoice->creator_id; // 42
TimeDiff — Date/time scopes and human-readable diffs
use Osoobe\Utilities\Traits\TimeDiff;
class JobPost extends Model
{
use TimeDiff;
}
// Scopes
JobPost::createdToday()->get();
JobPost::createdSinceWeek()->get();
JobPost::recentlyCreated(3)->get(); // last 3 days
JobPost::recentlyCreated(2, 'subWeeks')->get(); // last 2 weeks
JobPost::betweenDates('created_at', $start, $end)->get();
JobPost::expired()->get();
JobPost::notExpired()->get();
// Accessor
echo $post->created_time_diff; // "3 days ago"
// Instance methods
$post->expireInDays(30);
$post->expireInHours(48);
if ($post->isExpired()) { ... }
if ($post->recentlyCreated(1)) { ... }
HasFullTextSearch / FullTextSearchTrait — MySQL FULLTEXT search
Add the FULLTEXT index in your migration using MigrationHelper, then apply the trait to the model.
// In migration
use Osoobe\Utilities\Helpers\MigrationHelper;
public function up()
{
MigrationHelper::addFullTextSearch('articles', ['title', 'body'], 'articles_search');
}
use Osoobe\Utilities\Traits\FullTextSearchTrait;
class Article extends Model
{
use FullTextSearchTrait;
}
// Natural language search
Article::fullTextSearch(['title', 'body'], 'laravel utilities')->get();
// Boolean mode
Article::fullTextSearch(['title', 'body'], '+laravel -utilities', true)->get();
// OR version (FullTextSearchTrait only)
Article::orFullTextSearch(['title', 'body'], 'alternative terms')->get();
// Include relevance score in SELECT
$score = Article::selectFTSScore(['title', 'body'], 'laravel');
Article::selectRaw($score)->orderByDesc('fts_score')->get();
AdvanceWhereQuery — Conditional where clauses
use Osoobe\Utilities\Traits\AdvanceWhereQuery;
class Order extends Model
{
use AdvanceWhereQuery;
}
// Applies the WHERE only when $value is non-empty; skips it otherwise
Order::whereKeyOrNull('status', $request->status)->get();
ModelDefaultTrait — Set default values before create
use Osoobe\Utilities\Traits\ModelDefaultTrait;
class Subscription extends Model
{
use ModelDefaultTrait;
public function defaultModelValues(): void
{
$this->status = $this->status ?? 'trial';
$this->trial_ends_at = $this->trial_ends_at ?? now()->addDays(14);
}
}
SEO — SEO attribute contract
use Osoobe\Utilities\Traits\SEO;
class Product extends Model
{
use SEO;
public function getRouteURL(): string
{
return route('products.show', $this->slug);
}
public function getSEOTitleAttribute(): string
{
return $this->name . ' | My Shop';
}
public function getSEODescriptionAttribute(): string
{
return $this->summary;
}
}
// $product->url returns getRouteURL()
// $product->seo_title and $product->seo_description are accessible as attributes
Lang — Locale-based scopes
Requires a lang column and an App\Language model in the consuming app.
use Osoobe\Utilities\Traits\Lang;
class Post extends Model
{
use Lang;
}
Post::lang()->get(); // WHERE lang = current locale
Post::langEN()->get(); // WHERE lang = 'en'
Helper Classes
All helpers use static methods and require no instantiation.
Utilities
General-purpose utilities for objects, arrays, math, and strings.
use Osoobe\Utilities\Helpers\Utilities;
// Safe object/array access
$name = Utilities::getObjectValue($user, 'name', 'Guest');
$name = Utilities::getObjectValue($user, ['display_name', 'name'], 'Guest'); // tries each key
$val = Utilities::getArrayValue($data, 'key', 'default');
// Only set if the value is non-empty
Utilities::setObjectValue($model, 'bio', $request->bio);
Utilities::setArrayValue($data, 'phone', $request->phone);
Utilities::setArrayValueIfEmpty($settings, 'theme', 'light');
// Math
Utilities::calcNumberPercentage(15, 200); // 15% of 200 = 30
Utilities::calc_percentage(30, 200); // 30/200 as percentage = 15
Utilities::calcAverage(10.0, 20.0, 30.0); // 20.0
Utilities::calcAverageNoZeros(0, 10.0, 20.0); // ignores zeros
// Phone
Utilities::formatPhoneNumber('5551234567'); // "+15551234567"
Utilities::formatPhoneNumber('+15551234567'); // "+15551234567"
// Compare two Eloquent models
Utilities::model_compare($userA, $userB); // true if same class and id
// Email variation regex (handles dots and + aliases)
$regex = Utilities::getEmailVariationRegex('john.doe+tag@gmail.com');
// Array helpers
Utilities::isAssociativeArray(['a' => 1]); // true
Utilities::toAssociativeArray(['a', 'b']); // ['a' => 'a', 'b' => 'b']
Utilities::removeEmpty([0, '', null, 'hello']); // [0, 'hello']
// CSV
$rows = Utilities::csvToArray('/path/to/file.csv');
Utilities::outputCSV('/path/to/output.csv', $rows);
Str
Extends Illuminate\Support\Str — all built-in Laravel string methods are available.
use Osoobe\Utilities\Helpers\Str;
Str::ucwords('hello world'); // "Hello World"
Str::ucsnake('helloWorld'); // "Hello_World"
Str::boolToString(true); // "Yes"
Str::boolToString(false, 'B'); // "False"
$parts = Str::nameParts('John Michael Doe');
// $parts->first_name = "John"
// $parts->middle_name = "Michael"
// $parts->last_name = "Doe"
FormatHelper
Renders URLs, emails, and phone numbers as plain text, HTML anchors, or Markdown links.
use Osoobe\Utilities\Helpers\FormatHelper;
// Auto-detects type
FormatHelper::formatString('https://example.com', 'html');
// <a class='lm-format' href='https://example.com'>https://example.com</a>
FormatHelper::formatString('user@example.com', 'markdown', 'Contact');
// [Contact](mailto:user@example.com)
FormatHelper::formatPhone('+15551234567', 'html', 'Call Us');
// <a class='lm-format' href='tel:+15551234567'>Call Us</a>
FormatHelper::formatEmail('user@example.com', 'markdown');
// [user@example.com](mailto:user@example.com)
Supported $format values: 'html', 'markdown', 'string' (default).
Date
use Osoobe\Utilities\Helpers\Date;
$period = Date::getStartEndDate(Carbon::now(), 'weekly');
// $period->start_date (start of week)
// $period->end_date (end of week)
// $period->date (the input date)
// Supported periods: 'daily'/'day', 'weekly'/'week', 'monthly'/'month'
Date::isBetweenPeriod(Carbon::now(), 'monthly'); // true if today is within the current month
MigrationHelper
use Osoobe\Utilities\Helpers\MigrationHelper;
// Add a MySQL FULLTEXT index
MigrationHelper::addFullTextSearch('products', ['name', 'description'], 'products_search');
PhoneNumberHelper
Validation pattern is configurable via config('validation.phone.pattern').
use Osoobe\Utilities\Helpers\PhoneNumberHelper;
PhoneNumberHelper::isValid('+1 (555) 123-4567'); // true
PhoneNumberHelper::isValid('123'); // false (too short)
ImageHelper
use Osoobe\Utilities\Helpers\ImageHelper;
// Store a base64 image from an API request or form upload
$path = ImageHelper::storeBase64Image(
$request->avatar, // data:image/png;base64,...
'avatars', // directory
'user-42', // filename (extension auto-detected)
'public', // filesystem disk
'public' // visibility
);
// Returns the stored path, e.g. "avatars/user-42.png", or false on failure
// Strip the data URI prefix
$raw = ImageHelper::base64Only('data:image/jpeg;base64,/9j/4AAQ...');
MapBoxHelper
use Osoobe\Utilities\Helpers\MapBoxHelper;
$data = MapBoxHelper::queryLocationData(config('services.mapbox.key'), '1600 Pennsylvania Ave NW, Washington DC');
$cords = MapBoxHelper::getCordsFromData($data);
// $cords->latitude, $cords->longitude
$address = MapBoxHelper::getAddressComponent($data);
// $address->street_address, ->city, ->state, ->state_short,
// ->country, ->country_short, ->zip_code, ->latitude, ->longitude, ->full_address
// Reverse geocode
$data = MapBoxHelper::queryCoordinates(config('services.mapbox.key'), 38.8977, -77.0365);
GoogleMapsHelper
use Osoobe\Utilities\Helpers\GoogleMapsHelper;
$cords = GoogleMapsHelper::getGoogleMapCordinates(
config('services.google.maps_key'),
'1600 Pennsylvania Ave NW Washington DC'
);
// ['latitude' => 38.897..., 'longitude' => -77.036...]
Generic AJAX Resource Endpoint
The package provides a single parameterized route that serves model data to front-end libraries (Bootstrap Table, Select2) without writing per-model controllers or routes.
Route: GET /api/resources/{slug}/{format}
Name: api.resource.get
Formats: bst (Bootstrap Table, default), select2
1. Publish and configure api-endpoints.php
Copy the config file to your application:
php artisan vendor:publish --provider="Osoobe\Utilities\UtilitiesServiceProvider"
Then define each resource in config/api-endpoints.php:
return [
'users' => [
'model' => \App\Models\User::class,
'id_column' => 'id',
'text_column' => 'name',
'full_text_search' => ['name', 'email'],
'includes' => ['id', 'name', 'email', 'avatar_url'],
'conditions' => ['is_active' => 1],
// 'middleware' => ['auth:sanctum'],
// 'helper' => fn($request, $query, $configs) => $query->withRole('editor'),
],
'categories' => [
'model' => \App\Models\Category::class,
'id_column' => 'id',
'text_column' => 'name',
'includes' => ['id', 'name', 'slug'],
'conditions' => [],
],
];
2. Call the endpoint from the front end
Bootstrap Table:
$('#data-table').bootstrapTable({
url: '/api/resources/users/bst',
queryParams: params => ({ ...params, search: params.searchText }),
});
Response shape:
{ "rows": [...], "total": 120 }
Select2:
$('#user-select').select2({
ajax: {
url: '/api/resources/users/select2',
dataType: 'json',
data: params => ({ term: params.term, paginate: true }),
}
});
Response shape:
{ "results": [{ "id": 1, "text": "Jane Doe", "email": "jane@example.com" }, ...] }
Query parameters supported
| Parameter | Description |
|---|---|
term / search |
Text search (FULLTEXT or LIKE depending on config) |
sort |
Column to sort by (default: id) |
order |
asc or desc (default: asc) |
limit |
Number of records |
offset |
Pagination offset (Bootstrap Table) |
paginate |
true to enable cursor pagination (Select2) |
Custom Validators
Two validation rules are registered automatically.
phone
$request->validate([
'mobile' => 'required|phone',
]);
Override the pattern in config/validation.php:
'phone' => [
'pattern' => '%^[+]*[(]{0,1}[0-9]{1,4}[)]{0,1}[-\s\./0-9]*$%i',
],
password
$request->validate([
'password' => 'required|min:8|password',
]);
Requires the consuming app to publish a config/validation.php with:
'password' => [
'pattern' => '^(?=.*[A-Z])(?=.*\d).{8,}$',
'message' => 'The :attribute must contain at least one uppercase letter and one number.',
],
Base Artisan Command
Extend Osoobe\Utilities\Console\Command to get automatic execution timing.
use Osoobe\Utilities\Console\Command;
class SyncInventory extends Command
{
protected $timer = true; // prints "Took 2 minutes to complete" after handle()
protected $signature = 'inventory:sync';
public function handle()
{
// long-running work
}
}
Use Cases
Multi-tenant CRM with creator tracking
Use Userstamp + Active to track who created/edited every record and filter inactive ones:
class Contact extends Model
{
use Userstamp, Active;
}
// In migration
$table->userstamp();
$table->isActive();
// Query active contacts created by a specific user
Contact::active()->where('creator_id', auth()->id())->get();
Job board with expiry and full-text search
Use TimeDiff + FullTextSearchTrait for a job listing model:
class JobPost extends Model
{
use TimeDiff, FullTextSearchTrait;
}
// Active, non-expired listings matching a keyword, sorted by relevance
$score = JobPost::selectFTSScore(['title', 'description'], $keyword);
JobPost::notExpired()
->selectRaw("*, $score")
->fullTextSearch(['title', 'description'], $keyword)
->orderByDesc('fts_score')
->get();
Geocoding an address on model save
Combine MapBoxHelper with a model observer or ModelDefaultTrait:
class Property extends Model
{
use ModelDefaultTrait;
public function defaultModelValues(): void
{
if ($this->street_address && !$this->latitude) {
$data = MapBoxHelper::queryLocationData(
config('services.mapbox.key'),
$this->street_address . ', ' . $this->city
);
$cords = MapBoxHelper::getCordsFromData($data);
if ($cords) {
$this->latitude = $cords->latitude;
$this->longitude = $cords->longitude;
}
}
}
}
Searchable admin panel with Bootstrap Table + Select2
Register your models in config/api-endpoints.php and drop in the JS widgets — no backend changes needed when adding new searchable models.
// config/api-endpoints.php
'products' => [
'model' => \App\Models\Product::class,
'id_column' => 'id',
'text_column' => 'name',
'full_text_search' => ['name', 'sku', 'description'],
'includes' => ['id', 'name', 'sku', 'price'],
'conditions' => ['is_active' => 1],
'middleware' => ['auth:sanctum'],
],
// Bootstrap Table
$('#products-table').bootstrapTable({ url: '/api/resources/products/bst' });
// Select2 product picker
$('#product-picker').select2({ ajax: { url: '/api/resources/products/select2' } });
Storing user avatar uploads as base64
public function updateAvatar(Request $request)
{
$path = ImageHelper::storeBase64Image(
$request->input('avatar'),
'avatars/' . auth()->id(),
'profile',
'public',
'public'
);
if ($path) {
auth()->user()->update(['avatar_path' => $path]);
}
}
SEO-ready product pages
class Product extends Model
{
use SEO, HasSlug;
public function getRouteURL(): string
{
return route('products.show', $this->slug);
}
public function getSEOTitleAttribute(): string
{
return $this->name . ' — ' . config('app.name');
}
public function getSEODescriptionAttribute(): string
{
return Str::limit($this->description, 155);
}
}
{{-- In layout --}}
<title>{{ $product->seo_title }}</title>
<meta name="description" content="{{ $product->seo_description }}">
<link rel="canonical" href="{{ $product->url }}">
Testing
vendor/bin/phpunit
# Single file
vendor/bin/phpunit tests/Helpers/UtilitiesTest.php
Tests extend Osoobe\Utilities\Tests\TestCase (Orchestra Testbench), which auto-registers the service provider.
Changelog
Please see CHANGELOG for recent changes.
Contributing
Please see CONTRIBUTING for details.
Security
If you discover any security-related issues, please email b4.oshany@gmail.com instead of using the issue tracker.
License
The MIT License (MIT). Please see License File for more information.