osoobe / laravel-utilities
A description for utilities.
Requires
- guzzlehttp/guzzle: ^7.3
- illuminate/support: ^6.0|^7.0|^8.0|^9.0|^10.0
Requires (Dev)
- orchestra/testbench: ^6.0
This package is auto-updated.
Last update: 2026-05-16 22:44:11 UTC
README
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.