This guide walks you through common extension scenarios for Cashify.
Adding New Features
Creating a New Resource
Follow Laravel’s resource controller pattern to add new features.
Create Migration
Generate a migration for your new table: php artisan make:migration create_budgets_table
Define your schema: database/migrations/xxxx_create_budgets_table.php
use App\Models\ User ;
use App\Models\ Category ;
Schema :: create ( 'budgets' , function ( Blueprint $table ) {
$table -> id ();
$table -> foreignIdFor ( User :: class ) -> constrained () -> cascadeOnDelete ();
$table -> foreignIdFor ( Category :: class ) -> constrained () -> cascadeOnDelete ();
$table -> decimal ( 'amount' , 15 , 2 );
$table -> enum ( 'period' , [ 'daily' , 'weekly' , 'monthly' , 'yearly' ]);
$table -> timestamps ();
$table -> index ([ 'user_id' , 'category_id' ]);
});
Create Model
Generate the Eloquent model: php artisan make:model Budget
Define relationships and fillable fields: <? php
namespace App\Models ;
use Illuminate\Database\Eloquent\Factories\ HasFactory ;
use Illuminate\Database\Eloquent\ Model ;
use Illuminate\Database\Eloquent\Relations\ BelongsTo ;
class Budget extends Model
{
use HasFactory ;
protected $fillable = [
'amount' ,
'period' ,
'category_id' ,
];
public function user () : BelongsTo
{
return $this -> belongsTo ( User :: class );
}
public function category () : BelongsTo
{
return $this -> belongsTo ( Category :: class );
}
}
Create Controller
Generate a resource controller: php artisan make:controller BudgetController --resource
Implement CRUD operations: app/Http/Controllers/BudgetController.php
<? php
namespace App\Http\Controllers ;
use App\Models\ Budget ;
use Illuminate\Http\ Request ;
use Illuminate\Support\Facades\ Auth ;
class BudgetController extends Controller
{
public function index ()
{
$budgets = Budget :: query ()
-> with ([ 'category' ])
-> where ( 'user_id' , Auth :: id ())
-> latest ()
-> get ();
return view ( 'budgets.index' , compact ( 'budgets' ));
}
public function store ( Request $request )
{
$attributes = $request -> validate ([
'category_id' => 'required|exists:categories,id' ,
'amount' => 'required|numeric|min:0' ,
'period' => 'required|in:daily,weekly,monthly,yearly' ,
]);
Auth :: user () -> budgets () -> create ( $attributes );
flashToast ( 'success' , __ ( 'Budget created successfully.' ));
return redirect () -> route ( 'budgets.index' );
}
}
Add Routes
Register routes in routes/web.php: use App\Http\Controllers\ BudgetController ;
Route :: middleware ([ 'auth' , 'verified' ]) -> group ( function () {
Route :: controller ( BudgetController :: class ) -> group ( function () {
Route :: get ( '/budgets' , 'index' ) -> name ( 'budgets.index' );
Route :: get ( '/budgets/create' , 'create' ) -> name ( 'budgets.create' );
Route :: post ( '/budgets' , 'store' ) -> name ( 'budgets.store' );
Route :: get ( '/budgets/{budget}/edit' , 'edit' )
-> name ( 'budgets.edit' )
-> can ( 'update' , 'budget' );
Route :: patch ( '/budgets/{budget}' , 'update' )
-> name ( 'budgets.update' )
-> can ( 'update' , 'budget' );
Route :: delete ( '/budgets/{budget}' , 'destroy' )
-> name ( 'budgets.destroy' )
-> can ( 'delete' , 'budget' );
});
});
Create Views
Create Blade templates in resources/views/budgets/: resources/views/budgets/index.blade.php
< x-app-layout >
< div class = "max-w-7xl mx-auto" >
< x-panels.panel padding = "p-4" >
< div class = "flex justify-between items-center mb-4" >
< x-panels.heading > {{ __ ( 'Budgets' ) }} </ x-panels.heading >
< a href = " {{ route ('budgets.create') }} " >
< x-buttons.primary > {{ __ ( 'Add Budget' ) }} </ x-buttons.primary >
</ a >
</ div >
< div class = "space-y-4" >
@foreach ( $budgets as $budget )
< div class = "flex justify-between items-center p-4 bg-gray-50 dark:bg-gray-800 rounded-lg" >
< div >
< h3 class = "font-semibold" > {{ $budget -> category -> name }} </ h3 >
< p class = "text-sm text-gray-600 dark:text-gray-400" >
{{ ucfirst ( $budget -> period ) }} budget
</ p >
</ div >
< div class = "text-right" >
< p class = "font-bold" > {{ Number :: currency ( $budget -> amount , 'BGN' ) }} </ p >
</ div >
</ div >
@endforeach
</ div >
</ x-panels.panel >
</ div >
</ x-app-layout >
Add Navigation
Update the navigation menu to include your new feature: resources/views/components/nav/link.blade.php
< x-nav.link :href = " route ( 'budgets.index' ) " :active = " request () -> routeIs ( 'budgets.*' ) " >
< x-icon > wallet </ x-icon >
{{ __ ( 'Budgets' ) }}
</ x-nav.link >
Adding Custom Categories
Programmatically Adding Categories
You can add categories in seeders, listeners, or controllers:
use App\Models\ Category ;
$user -> categories () -> create ([
'name' => 'Subscriptions' ,
'type' => 'expense' ,
'color' => 'purple' ,
'icon' => 'subscription' ,
]);
Modifying Default Categories
Edit the default categories configuration:
config/default-categories.php
return [
// Add your custom defaults
[
'name' => 'Freelance Work' ,
'type' => 'income' ,
'color' => 'emerald' ,
'icon' => 'laptop'
],
[
'name' => 'Coffee' ,
'type' => 'expense' ,
'color' => 'amber' ,
'icon' => 'coffee-cup'
],
// ... existing categories
];
Adding Custom Category Icons
Add Icon File
Place your SVG icon in public/images/categories/: public/images/categories/my-custom-icon.svg
Use in Category
Reference the icon filename (without extension): 'icon' => 'my-custom-icon'
Icon files are automatically discovered from the public/images/categories/ directory and displayed in the category creation form.
Adding New Transaction Types
Currently, Cashify supports four transaction types: income, expense, correction, and transfer.
Adding a New Type
Update Migration
Modify the categories table enum: $table -> enum ( 'type' , [ 'income' , 'expense' , 'correction' , 'transfer' , 'investment' ]);
Create and run the migration: php artisan make:migration add_investment_type_to_categories
php artisan migrate
Update Transaction Logic
Modify the TransactionController to handle the new type: app/Http/Controllers/TransactionController.php
public function store ( TransactionRequest $request ) : RedirectResponse
{
$attributes = $request -> validated ();
$category = Category :: findOrFail ( $attributes [ 'category_id' ]);
// Handle different transaction types
if ( $category -> type == 'expense' ) {
$attributes [ 'amount' ] = - abs ( $attributes [ 'amount' ]);
} elseif ( $category -> type == 'investment' ) {
// Custom logic for investments
$attributes [ 'amount' ] = - abs ( $attributes [ 'amount' ]);
// Maybe track investment separately
}
// ... rest of the logic
}
Update Views
Add UI for the new transaction type in forms: < x-tabs.button >
{{ __ ( 'Investment' ) }}
< x-icon class = "text-blue-500 mt-1" > trending_up </ x-icon >
</ x-tabs.button >
Custom Filters
Extend the TransactionFilter class to add custom filtering logic:
app/Filters/TransactionFilter.php
protected function applyAccountFilter ( Builder $query ) : void
{
if ( $this -> request -> filled ( 'accounts' )) {
$accountIds = $this -> request -> input ( 'accounts' );
$query -> whereIn ( 'account_id' , $accountIds );
}
}
protected function applyCustomDateFilter ( Builder $query ) : void
{
if ( $this -> request -> filled ( 'preset_range' )) {
$preset = $this -> request -> input ( 'preset_range' );
match ( $preset ) {
'today' => $query -> whereDate ( 'created_at' , today ()),
'this_week' => $query -> whereBetween ( 'created_at' , [
now () -> startOfWeek (),
now () -> endOfWeek ()
]),
'this_month' => $query -> whereMonth ( 'created_at' , now () -> month ),
'this_year' => $query -> whereYear ( 'created_at' , now () -> year ),
default => null ,
};
}
}
public function apply ( Builder $query ) : Builder
{
$this -> applyTypeFilter ( $query );
$this -> applyCategoryFilter ( $query );
$this -> applyAccountFilter ( $query ); // New filter
$this -> applyCustomDateFilter ( $query ); // New filter
$this -> applyAmountFilter ( $query );
$this -> applyTitleFilter ( $query );
$this -> applyDetailsFilter ( $query );
$this -> applyDateRangeFilter ( $query );
return $query ;
}
Creating Custom Charts
Add new chart classes following the existing pattern:
app/Charts/IncomeVsExpenseChart.php
<? php
namespace App\Charts ;
use App\Models\ Transaction ;
use Illuminate\Support\Facades\ Auth ;
use Illuminate\Support\Facades\ DB ;
class IncomeVsExpenseChart
{
public function build () : array
{
$monthlyData = Transaction :: query ()
-> select (
DB :: raw ( 'DATE_FORMAT(created_at, "%Y-%m") as month' ),
DB :: raw ( 'SUM(CASE WHEN categories.type = "income" THEN amount ELSE 0 END) as income' ),
DB :: raw ( 'SUM(CASE WHEN categories.type = "expense" THEN ABS(amount) ELSE 0 END) as expense' )
)
-> join ( 'categories' , 'transactions.category_id' , '=' , 'categories.id' )
-> where ( 'transactions.user_id' , Auth :: id ())
-> groupBy ( 'month' )
-> orderBy ( 'month' )
-> limit ( 12 )
-> get ();
return [
'labels' => $monthlyData -> pluck ( 'month' ) -> toArray (),
'income' => $monthlyData -> pluck ( 'income' ) -> toArray (),
'expense' => $monthlyData -> pluck ( 'expense' ) -> toArray (),
];
}
}
Use in your controller:
app/Http/Controllers/DashboardController.php
public function __invoke ( IncomeVsExpenseChart $chart )
{
$chartData = $chart -> build ();
return view ( 'dashboard' , [
'chartData' => $chartData ,
]);
}
Adding Middleware
Create custom middleware for features like spending limits:
app/Http/Middleware/CheckSpendingLimit.php
<? php
namespace App\Http\Middleware ;
use Closure ;
use Illuminate\Http\ Request ;
use Illuminate\Support\Facades\ Auth ;
class CheckSpendingLimit
{
public function handle ( Request $request , Closure $next )
{
$user = Auth :: user ();
// Get monthly spending
$monthlySpending = abs (
$user -> transactions ()
-> whereHas ( 'category' , fn ( $q ) => $q -> where ( 'type' , 'expense' ))
-> whereMonth ( 'created_at' , now () -> month )
-> sum ( 'amount' )
);
$limit = $user -> monthly_limit ?? 10000 ; // Default limit
if ( $monthlySpending >= $limit ) {
session () -> flash ( 'warning' , 'You have reached your monthly spending limit!' );
}
return $next ( $request );
}
}
Register in app/Http/Kernel.php:
protected $middlewareAliases = [
// ... existing middleware
'spending.limit' => \App\Http\Middleware\ CheckSpendingLimit :: class ,
];
Apply to routes:
Route :: middleware ([ 'auth' , 'verified' , 'spending.limit' ]) -> group ( function () {
// Your routes
});
Custom Traits
Create reusable functionality with traits:
app/Traits/HasNotifications.php
<? php
namespace App\Traits ;
trait HasNotifications
{
public function sendBudgetAlert ( string $categoryName , float $spent , float $budget )
{
$percentage = ( $spent / $budget ) * 100 ;
if ( $percentage >= 90 ) {
flashToast ( 'warning' ,
__ ( "You've spent :percent% of your :category budget!" , [
'percent' => round ( $percentage ),
'category' => $categoryName
])
);
}
}
}
Use in models:
class User extends Authenticatable
{
use HasFactory , Notifiable , HasNotifications ;
// ...
}
Events and Listeners
Create custom events for extending functionality:
Create Event
Create Listener
Register
php artisan make:event TransactionCreated
app/Events/TransactionCreated.php
<? php
namespace App\Events ;
use App\Models\ Transaction ;
use Illuminate\Foundation\Events\ Dispatchable ;
use Illuminate\Queue\ SerializesModels ;
class TransactionCreated
{
use Dispatchable , SerializesModels ;
public function __construct ( public Transaction $transaction )
{}
}
php artisan make:listener CheckBudgetLimit
app/Listeners/CheckBudgetLimit.php
<? php
namespace App\Listeners ;
use App\Events\ TransactionCreated ;
use Illuminate\Contracts\Queue\ ShouldQueue ;
class CheckBudgetLimit implements ShouldQueue
{
public function handle ( TransactionCreated $event ) : void
{
$transaction = $event -> transaction ;
if ( $transaction -> category -> type !== 'expense' ) {
return ;
}
// Check budget logic here
// Send notifications, update stats, etc.
}
}
app/Providers/EventServiceProvider.php
protected $listen = [
TransactionCreated :: class => [
CheckBudgetLimit :: class ,
UpdateDashboardCache :: class ,
],
];
Helper Functions
Add custom helpers to app/helpers.php:
if ( ! function_exists ( 'formatCurrency' )) {
function formatCurrency ( float $amount , string $currency = 'BGN' ) : string
{
return Number :: currency ( $amount , in : $currency , locale : app () -> getLocale ());
}
}
if ( ! function_exists ( 'getMonthlySpending' )) {
function getMonthlySpending ( ? int $userId = null ) : float
{
$userId = $userId ?? Auth :: id ();
return abs (
Transaction :: where ( 'user_id' , $userId )
-> whereHas ( 'category' , fn ( $q ) => $q -> where ( 'type' , 'expense' ))
-> whereMonth ( 'created_at' , now () -> month )
-> sum ( 'amount' )
);
}
}
if ( ! function_exists ( 'calculateSavingsRate' )) {
function calculateSavingsRate ( ? int $userId = null ) : float
{
$userId = $userId ?? Auth :: id ();
$income = Transaction :: where ( 'user_id' , $userId )
-> whereHas ( 'category' , fn ( $q ) => $q -> where ( 'type' , 'income' ))
-> whereMonth ( 'created_at' , now () -> month )
-> sum ( 'amount' );
$expenses = abs (
Transaction :: where ( 'user_id' , $userId )
-> whereHas ( 'category' , fn ( $q ) => $q -> where ( 'type' , 'expense' ))
-> whereMonth ( 'created_at' , now () -> month )
-> sum ( 'amount' )
);
if ( $income == 0 ) return 0 ;
return (( $income - $expenses ) / $income ) * 100 ;
}
}
Best Practices
Follow Laravel Conventions
Use resource controllers for CRUD operations
Follow PSR-12 coding standards
Use Eloquent relationships over raw queries
Leverage service containers for dependency injection
Always use database transactions for multi-step operations
Implement proper validation using Form Requests
Use foreign key constraints with appropriate cascade rules
Keep monetary values as decimals, never floats
Use Laravel’s authorization policies for access control
Validate all user input with Form Requests
Use CSRF protection (automatic with Blade forms)
Sanitize output in views (automatic with Blade )
Before making database changes in production, always create backups and test migrations in a staging environment.