Skip to main content
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.
1

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']);
});
2

Create Model

Generate the Eloquent model:
php artisan make:model Budget
Define relationships and fillable fields:
app/Models/Budget.php
<?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);
    }
}
3

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');
    }
}
4

Add Routes

Register routes in routes/web.php:
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');
    });
});
5

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>
6

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

1

Add Icon File

Place your SVG icon in public/images/categories/:
public/images/categories/my-custom-icon.svg
2

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

1

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
2

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
}
3

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:
routes/web.php
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:
app/Models/User.php
class User extends Authenticatable
{
    use HasFactory, Notifiable, HasNotifications;
    
    // ...
}

Events and Listeners

Create custom events for extending functionality:
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)
    {}
}

Helper Functions

Add custom helpers to app/helpers.php:
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

  • 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 eager loading to prevent N+1 queries
  • Add database indexes for frequently queried columns
  • Implement caching for expensive calculations
  • Queue heavy operations using Laravel’s queue system
  • 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.

Build docs developers (and LLMs) love