Zephyr Shared is a foundational package that delivers shared logic, UI components, and developer tools across all Zephyr-IT modular Laravel applications.
It provides:
- A reusable
BasePluginsystem for Filament v3 (auto-register pages, resources, widgets) - Pre-configured Filament resources: Country, State, and City
- Export architecture for multi-sheet Excel reports using Laravel Excel
- A CLI script to check for missing language keys
- Centralized helpers, traits, and support classes for common functionality
- Installation
- Usage
- Excel Report Exports
- Global Helper Functions
- Model Concerns
- UniqueEncryptedRule
- Smart Seeding System with BaseSeeder
- Useful Shared Traits
- Dynamic Metrics Engine
- How to Use
- Helper Methods (Internal Use)
- Testing
- Changelog
- Contributing
- Security
- Credits
- License
Install via Composer:
composer require zephyr-it/sharedAfter installing the package, publish all shared configuration files, assets, migrations, and utilities by running:
php artisan shared:installThis command will:
- Publish shared configs, views, migrations, and scripts
- Publish third-party assets like
spatie/laravel-activitylogmigrations - Prompt you to run
php artisan migrate
To overwrite existing files:
php artisan shared:install --forceℹ️ This is the preferred way to initialize the shared stack within any Laravel application or module using Zephyr-IT’s architecture.
This package offers a powerful and flexible foundation for building multi-sheet Excel reports using Laravel Excel (maatwebsite/excel).
With built-in support for:
- 🎨 Styled sheets and column formatting
- 📚 Auto-generated legends and notes
- 🎯 Custom filters, headings, and merged cells
- 📊 Modular architecture for reusable reporting logic
✅ Laravel Excel is already required. No additional setup is necessary.
Defines a single Excel sheet. Implements:
FromCollection,WithHeadings,WithStylesWithEvents,WithTitle,WithColumnFormattingShouldQueue,ShouldAutoSize
Ideal for building modular, stylized reports.
Used to group and manage multiple BaseSheet instances.
Also provides integrated date range parsing via the HasDateRangeParser trait.
new CompanyUsersExport(
$company,
'2024-01-01 - 2024-01-31',
$extraContext
);new UsersSheet(
title: 'Users',
data: $collection,
headings: ['ID', 'Name'],
headerColor: '1E88E5',
legendRows: [['Column', 'Details']],
legendStyles: [],
columnWidths: ['A' => 15, 'B' => 30],
mergeCells: ['A1:C1'],
rowStyles: [],
applyBorders: true,
freezeHeader: true,
useAutoFilter: true,
enableLogging: true,
notesRow: 'Data generated from system.'
)| Feature | Description |
|---|---|
notesRow |
Adds an intro row above headings for metadata or descriptions |
legendRows |
Displays a table legend for context (e.g., column definitions) |
legendStyles |
Style individual cells in the legend |
columnWidths |
Specify manual widths for columns |
mergeCells |
Merge specific cell ranges |
rowStyles |
Apply font, color, or alignment styles to specific rows |
headerColor |
Adds a background color and white text to the heading row |
useAutoFilter |
Enables Excel dropdown filters on the heading row |
freezeHeader |
Locks the top row while scrolling |
enableLogging |
Logs export metadata to storage/logs/laravel.log |
You can override columnFormats() and styles() in your custom sheet:
public function columnFormats(): array
{
return [
'C' => '#,##0.00',
'D' => 'dd-mm-yyyy',
];
}
public function styles(Worksheet $sheet): array
{
return [
2 => ['font' => ['bold' => true]],
];
}Override registerEvents() in your BaseSheet class to:
- Customize borders
- Dynamically merge cells
- Alter freeze pane logic
- Inject
notesRoworlegendRowsconditionally
class UsersSheet extends BaseSheet
{
public function columnFormats(): array
{
return ['C' => '#,##0.00'];
}
}class CompanyUsersExport extends AbstractReportExport
{
public function sheets(): array
{
return [
new UsersSheet(
title: 'Users',
data: $this->entity->users->map(fn ($u) => [$u->id, $u->name, $u->balance]),
headings: ['ID', 'Name', 'Balance'],
notesRow: 'This report shows active users only.',
legendRows: [['Balance', 'User’s current account balance']],
)
];
}
}Immediate download:
return Excel::download(
new CompanyUsersExport($company, 'this_month'),
'company-users.xlsx'
);Queued export:
Excel::queue(
new CompanyUsersExport($company, 'last_30_days'),
'exports/company.xlsx'
);- Export buttons on Filament table listings
- Scheduled exports via Laravel scheduler
- Admin and compliance reporting
- API-based report generators
- Multi-sheet financial summaries
- Dynamic data audits filtered by date range
Zephyr Shared provides a powerful BasePlugin class to streamline plugin development for Filament v3. It enables clean, modular registration of your Pages, Resources, and Widgets — all based on convention.
- ✅ Auto-registers Filament components via reflection
- ✅ Detects correct namespaces and directories based on file location
- ✅ Keeps plugin definitions DRY and declarative
- ✅ Supports toggling discovery for Pages, Widgets, and Resources
To enable your plugin in the Filament panel:
use ZephyrIt\Accounts\Filament\SharedPlugin;
public function panel(Panel $panel): Panel
{
return $panel
->plugin(SharedPlugin::make());
}Want to skip registering specific components?
SharedPlugin::make()
->registerPages(false)
->registerWidgets(false);By using BasePlugin, all your Zephyr module plugins remain:
- ✅ Clean
- ✅ Auto-wired
- ✅ Convention-driven
- ✅ Consistently modular
Zephyr Shared includes a ReportPage base class to help you rapidly scaffold Filament-powered dashboard pages — complete with filters, permissions, and layout.
When extending ReportPage, you automatically get:
- 📅 A date range picker with today’s date as the default
- 🧭 Navigation label and title via
titleKey - 🗂 Grouping via
getNavigationGroup() - 🧱 Layout managed by Filament’s dashboard base
- 🔍 Optional custom filters via
getFilterFormSchema() - 🔐 Role-based access control with
HasPageShield
All helpers are available globally from src/Helpers/bootstrap_helpers.php.
numberToIndianFormat(1234567); // "12,34,567"
numberToWord(123); // "one hundred twenty-three"
formatNumberShort(1500000); // "1.5M"
formatAddButtonLabel('User'); // "Add User"
formatCopyMessage('Email'); // "Copied Email"sanitizeAndFormat('John DOE'); // "John Doe"
sanitizeSpecialCharacters('Hey#$', ' ') // "Hey"
normalizeString('Ärgerlich'); // "Argerlich"getEnumValues(StatusEnum::class); // ['active', 'inactive']
getEnumLabels(StatusEnum::class); // ['active' => 'Active', ...]
getFilteredEnumStatuses(StatusEnum::class, ['archived']);getActiveModules(); // ['cms', 'hrm', 'insurance']
getModuleModels('cms'); // ['Page', 'Post']
getAllModelPaths();
getModuleNamespace('cms'); // App\Modules\Cms
clearModuleCache();getAllModelClasses(); // Fully-qualified class names
resolveFieldCast(User::class, 'email'); // 'string'
getLastRecord(User::class, 'created_at'); // Latest created usergenerateUniqueNumber(User::class, 'USR', 'code'); // "USR/2025/001"
generateYears(2000, 2025); // [2000, ..., 2025]
getClassesFromDirectory(app_path('Models'), 'App\\Models');getCurrencySymbol(); // ₹
getDenominationsArray('currency');
transformSupportedLocales(); // ['en' => 'English', ...]Used in commands and setup logic
install_publish($this, [['tag' => 'toolkit-config']]);
install_publish_files($this, base_path('publish'));
install_publish_migrations($this, base_path('migrations'));A foundational Eloquent model that serves as the base for all Zephyr-IT models.
- Auto-tracks
fillablechanges usingSpatie\Activitylog - Lifecycle control via
HasLifecycleHooks - Easily extended with shared scopes (
HasCommonScopes)
use ZephyrIt\Shared\Models\BaseModel;
use Spatie\Activitylog\LogOptions;
class Invoice extends BaseModel
{
public function getActivitylogOptions(): LogOptions
{
return LogOptions::defaults()
->logFillable()
->logOnlyDirty();
}
}Extends Laravel's Authenticatable with the same lifecycle behaviors.
User,Admin,Staff, or other models with Laravel Auth
use ZephyrIt\Shared\Models\BaseAuthModel;
class Staff extends BaseAuthModel
{
// Includes auth + lifecycle logic
}Encapsulates transactional lifecycle control for Eloquent model events.
- Wraps create/update/save/delete/restore in DB transactions
- Uses simple overridable methods for each lifecycle phase
- Supports automatic cascade delete/restore for relationships
Enable cascade delete/restore on related models:
protected bool $shouldSyncRelatedSoftDeletes = true;
protected function relatedModelsForSoftDelete(): array
{
return ['tasks', 'comments'];
}You can override any of the following:
protected function performCreating(): void {}
protected function performAfterCreate(): void {}
protected function performUpdating(): void {}
protected function performAfterUpdate(): void {}
protected function performSaving(): void {}
protected function performAfterSave(): void {}
protected function performDeleting(): void {}
protected function performAfterDelete(): void {}
protected function performRestoring(): void {}
protected function performAfterRestore(): void {}All methods are optional — define only what you need.
use ZephyrIt\Shared\Models\BaseModel;
class Invoice extends BaseModel
{
protected bool $shouldSyncRelatedSoftDeletes = true;
protected function relatedModelsForSoftDelete(): array
{
return ['lineItems'];
}
public function lineItems()
{
return $this->hasMany(LineItem::class);
}
protected function performAfterUpdate(): void
{
AuditLogger::log("Invoice {$this->id} was updated.");
}
}A collection of reusable and schema-safe Eloquent query scopes.
- Add common filtering, searching, and ordering methods
- Skip queries safely if the expected column doesn’t exist
- Ideal for multi-tenant or optional schema scenarios
Every scope uses hasColumn() internally:
protected function hasColumn(Builder $query, string $column): boolEnsures the query only runs if the column is present.
| Scope | Description |
|---|---|
active() |
is_active = true (safe fallback) |
inactive() |
is_active = false |
whereBoolean() |
Generic boolean filter with column check |
whereStatus() |
Filters by a status value |
ordered() |
Order by any column + direction |
recent() |
Order by created_at DESC |
latestFirst() |
Most recent records by any column |
oldestFirst() |
Oldest records by any column |
search() |
LIKE %term% search on a single column |
User::active()
->whereStatus('verified')
->search('email', 'example.com')
->latestFirst()
->get();Each scope checks for the existence of the target column — meaning no more "column not found" errors in evolving or modular schemas.
This custom validation rule ensures encrypted fields remain unique across your models — even when stored as ciphertext.
It is designed specifically for cases where traditional unique: validation can't compare encrypted values in the database.
- You store fields like
email,phone, orSSNencrypted in the DB - You need to prevent duplicate entries without decrypting every row
- You want secure validation while editing (excluding the current model)
use ZephyrIt\Shared\Rules\UniqueEncryptedRule;
new UniqueEncryptedRule(User::class, 'email', $this->user, 'name');User::class— target model'email'— encrypted column$this->user— optional current model (for edit forms)'name'— optional display column for error messages
public function rules(): array
{
return [
'email' => [
'required',
new UniqueEncryptedRule(User::class, 'email', $this->user),
],
];
}Add this to your resources/lang/en/messages.php file to customize the validation error:
'unique_encrypted' => 'The :attribute already exists (used by :existing).',
:existingwill be replaced with the value from the optional "display column" (likenameor
The BaseSeeder class provides a safe, idempotent, and environment-aware seeding entrypoint for Zephyr-IT applications. It is used at the top level (DatabaseSeeder) to orchestrate and manage all module and development seeders.
❗ Module-specific seeders (e.g.
AccountsSeeder,UserSeeder) should extend the defaultIlluminate\Database\Seeder, notBaseSeeder.
- 📓 Logs seeder runs in the
seed_logtable to prevent duplicate execution - ♻️ Automatically suppresses
spatie/laravel-activitylogduring seeding - ⚙️ Smart
run()logic that separates production vs. development seeders - 🔁 Nested seeding supported via Laravel’s native
$this->call()system
Ensure the seed_log tracking table exists by running the shared installer:
php artisan shared:install
php artisan migrateNote Avoid using
vendor:publishdirectly. Theshared:installcommand handles publishing migrations and other assets in a single step.
namespace Database\Seeders;
use ZephyrIt\Shared\Support\BaseSeeder;
use ZephyrIt\Shared\Database\Seeders\WorldDatabaseSeeder;
class DatabaseSeeder extends BaseSeeder
{
public function run(): void
{
$this->command->info('🔄 Starting database seeding...');
$baseSeeders = [
WorldDatabaseSeeder::class,
ShieldSeeder::class,
UserSeeder::class,
PlanSeeder::class,
FeatureSeeder::class,
ProductSeeder::class,
];
$developmentSeeders = [
TenantSeeder::class,
// Add other dev/test seeders here
];
$seeders = app()->isProduction()
? $baseSeeders
: array_merge($baseSeeders, $developmentSeeders);
$this->call($seeders);
$this->command->info('✔ Database seeding completed.');
}
}namespace ZephyrIt\Accounts\Database\Seeders;
use Illuminate\Database\Seeder;
class AccountsSeeder extends Seeder
{
public function run(): void
{
$this->call([
AccountTypeSeeder::class,
DefaultAccountsSeeder::class,
]);
}
}This pattern keeps your module seeders lightweight and clean, using Laravel's core Seeder class.
To run shared or environment-specific seeders via DatabaseSeeder:
php artisan db:seedTo run a specific one:
php artisan db:seed --class="ZephyrIt\Shared\Database\Seeders\WorldDatabaseSeeder"- 🛡 Prevents duplicate seed execution using
seed_log - 🧘 Auto-suppresses activity logs during seeding
- 🔄 Restores log state even if an exception occurs
- 🧵 Supports full project orchestration without touching module logic
These traits help you keep your services, charts, and multi-tenant logic modular, DRY, and production-ready across Zephyr-IT modules.
Dynamically generate a consistent color palette for charts (e.g. Chart.js, ApexCharts) with fallback to random RGBA values.
- Returns an array of RGBA strings with optional opacity
- Ensures visual consistency using preconfigured base colors
- Auto-fills additional entries with randomized color variants
use ZephyrIt\Shared\Traits\ColorPaletteTrait;
class RevenueChartService
{
use ColorPaletteTrait;
public function getChartColors(int $count): array
{
return $this->getColors($count);
}
}[
'backgroundColor' => ['rgba(255, 99, 132, 0.7)', 'rgba(54, 162, 235, 0.7)', ...],
'borderColor' => ['rgba(255, 99, 132, 1)', 'rgba(54, 162, 235, 1)', ...],
]This array can be injected directly into chart configuration objects.
Resolves the current tenant based on the domain, using the stancl/tenancy package.
- Reads the current domain via
request()->getHost() - Queries the
domainstable to find the matchingTenant - Returns the resolved tenant model — or
nullif not found
use ZephyrIt\Shared\Traits\DeterminesTenant;
class TenantAwareReportService
{
use DeterminesTenant;
public function getTenantName(): ?string
{
return optional($this->determineTenant())->name;
}
}Returns null if php artisan is running (e.g. in queue workers, commands, or test runners).
A smart, flexible parser for handling UI-driven or backend-passed date ranges in a clean, uniform format.
[$start, $end] = $this->parseDateRange('2024-05-01 - 2024-05-31');
[$from, $to] = $this->parseDateRangeAsStrings(['01/05/2024', '31/05/2024']);'2024-05-01 - 2024-05-31'(string range)['2024-05-01', '2024-05-31'](array of ISO dates)['01/05/2024', '31/05/2024'](array of D/M/Y format)
This trait is extremely useful for:
- Report filtering
- Dashboard widgets
- Scheduled exports
- API endpoints expecting flexible date inputs
The DynamicMetricsTrait provides a flexible, powerful way to aggregate, filter, and chart dynamic data metrics across any set of models. It is ideal for use in Filament dashboards, admin reports, and time-based insights.
- Aggregate metrics (sum, count, list, avg) over time
- Group by single or nested keys (e.g., date → user → status)
- Chart-ready structure with interval-aware logic (daily/weekly/monthly)
- Optional
Closure-based filters and dynamic groupings - Works across multiple models in a single call
Fetches grouped, nested metric results across a time range.
$results = $this->fetchDynamicMetricsData(
models: [Invoice::class, Payment::class],
metrics: [
'total_amount' => ['amount', 'sum'],
'avg_settlement_days' => ['registered_date', 'avg', fn ($group) => $group->whereNotNull('settlement_date')],
],
startDate: now()->subMonth(),
endDate: now(),
groupByColumn: fn ($item) => [$item->created_at->format('Y-m'), $item->status]
);🧠 Output will be grouped by month + status, and metrics are aggregated accordingly.
Returns an array of values suitable for charting over time (daily, weekly, monthly).
$chartData = $this->getMetricChartData(
models: [Invoice::class],
metricType: 'sum',
column: 'amount',
startDate: now()->subMonths(3),
endDate: now()
);Returns something like:
[1200, 1500, 1800, 2100] // One value per intervalCalculates a final, formatted metric (e.g. grand total or count) over time and multiple models.
$totalRevenue = $this->getMetricData(
models: [Invoice::class],
metricType: 'sum',
column: 'amount',
startDate: now()->subQuarter(),
endDate: now()
);✅ Automatically returns formatted string using numberToIndianFormat() helper.
The trait automatically picks an interval based on date range:
- ≤ 7 days →
daily - ≤ 90 days →
weekly - ≤ 365 days →
monthly -
365 days →
quarterly
Override logic manually by customizing the determineDateInterval() method.
Use closures or arrays to apply conditional logic to a group:
'only_pending' => ['amount', 'sum', fn ($group) => $group->where('status', 'pending')]Or:
[
'field' => 'status',
'operator' => '=',
'value' => 'pending',
]Support multi-dimensional grouping via groupByColumn closure:
fn ($item) => [$item->created_at->format('Y-m'), $item->user_id]| Method | Description |
|---|---|
prepareDateRange() |
Defaults to current month if no date provided |
generateDateRange() |
Builds list of intervals for aggregation |
calculateIntervalMetric() |
Aggregates values within each interval |
deepMerge() |
Recursively merges numeric/array values |
recursiveGroupBy() |
Handles multi-dimensional grouping |
processMetricGroup() |
Applies aggregate functions on grouped data |
- Filament dashboard KPIs with date filters
- Line/bar/pie charts with
Chart.jsorApexCharts - Admin exports of grouped metrics (per user, per status, etc.)
- Aggregating metrics across multi-model data lakes (e.g.
Invoice,Payment,Transaction)
Install PHP dependencies and run the test suite with Pest:
composer install
composer testThe tests are configured via phpunit.xml.dist. This ensures that all traits, resources, commands, and integrations function as expected across environments.
To verify coding standards, run the style fixer:
composer lintThis uses PHP CS Fixer and Laravel Pint to enforce consistent formatting.
Refer to the CHANGELOG for a full history of updates, bug fixes, and feature releases.
We welcome contributions! Please review our CONTRIBUTING guide before submitting issues or pull requests.
If you discover any security vulnerabilities, please refer to our security policy for responsible disclosure guidelines.
- @abbasmashaddy72 – Project Lead
- Zephyr-IT Team – Core Maintainers
- All Contributors – Special thanks to everyone who has contributed!
This project is open-source software licensed under the MIT License.