Skip to main content
Cashify is built on a modern PHP stack that combines Laravel’s robust backend with HTMX for dynamic interactions and Alpine.js for reactive UI components.

Tech Stack

Backend

  • Laravel 11.x (PHP 8.2+)
  • MySQL/SQLite database
  • Laravel Breeze for authentication
  • Laravel Socialite for OAuth

Frontend

  • HTMX for dynamic interactions
  • Alpine.js for reactivity
  • Tailwind CSS for styling
  • ApexCharts for data visualization

MVC Architecture

Cashify follows Laravel’s Model-View-Controller (MVC) pattern with clear separation of concerns.

Models

The application uses Eloquent ORM for database interactions. All models are located in app/Models/.
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class Transaction extends Model
{
    use HasFactory;

    protected $guarded = [];

    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }

    public function account(): BelongsTo
    {
        return $this->belongsTo(Account::class);
    }

    public function category(): BelongsTo
    {
        return $this->belongsTo(Category::class);
    }
}

Controllers

Controllers handle HTTP requests and coordinate between models and views. Located in app/Http/Controllers/.
app/Http/Controllers/TransactionController.php
public function store(TransactionRequest $request): RedirectResponse
{
    $attributes = $request->validated();

    $account = Account::findOrFail($attributes['account_id']);
    $category = Category::findOrFail($attributes['category_id']);

    // Negative amounts for expenses
    if ($category->type == 'expense') {
        $attributes['amount'] = -abs($attributes['amount']);
    }

    $attributes['created_at'] = $attributes['created_at'] ?? now();

    // Update account balance
    $account->update([
        'balance' => $account->balance + $attributes['amount'],
    ]);

    Auth::user()->transactions()->create($attributes);

    updateNetworth();

    flashToast('success', __('Transaction created successfully.'));

    return Redirect::route('transactions.index');
}
Notice how the controller handles business logic like converting expenses to negative amounts and updating account balances automatically.

Views

Views are Blade templates located in resources/views/. Cashify uses reusable Blade components for consistent UI.
resources/views/transactions/create.blade.php
<x-app-layout>
    <form action="{{ route('transactions.store') }}" method="POST">
        @csrf

        <div class="w-full max-w-xl mx-auto">
            <x-panels.panel padding="p-4" class="space-y-6 mb-4">
                <x-panels.heading class="mb-2">{{__('Add Transaction')}}</x-panels.heading>

                <x-tabs.body class="flex flex-col gap-4">
                    <x-tabs.button-group>
                        <x-tabs.button>
                            {{__('Expense')}}
                            <x-icon class="text-red-500 mt-1">arrow_drop_down</x-icon>
                        </x-tabs.button>
                        <x-tabs.button>
                            {{__('Income')}}
                            <x-icon class="text-emerald-500 mt-1">arrow_drop_up</x-icon>
                        </x-tabs.button>
                    </x-tabs.button-group>
                </x-tabs.body>
            </x-panels.panel>
        </div>
    </form>
</x-app-layout>

HTMX Integration

Cashify uses HTMX to create dynamic, SPA-like experiences without complex JavaScript frameworks.

Laravel HTMX Package

The app uses mauricius/laravel-htmx package for seamless HTMX integration with Laravel.
app/Http/Controllers/CategoryController.php
use Mauricius\LaravelHtmx\Facades\HtmxResponse;
use Mauricius\LaravelHtmx\Http\HtmxRequest;

public function show(HtmxRequest $request, Category $category)
{
    if ($request->isHtmxRequest()) {
        return HtmxResponse::addFragment('categories.show', 'panel', [
            'category' => $category,
        ]);
    }

    return view('categories.show', [
        'category' => $category,
    ]);
}

Fragment Rendering

HTMX responses use Laravel’s fragment feature to render partial views:
resources/views/categories/show.blade.php
<x-app-layout>
    @fragment('panel')
        @include('categories.partials.show', ['category' => $category])
    @endfragment
</x-app-layout>
HTMX requests detect user interactions and return only the HTML fragments that need updating, reducing bandwidth and improving performance.

Advanced HTMX Responses

The CategoryController demonstrates advanced HTMX patterns:
app/Http/Controllers/CategoryController.php
public function update(CategoryRequest $request, Category $category)
{
    $oldType = $category->type;
    $category = Category::findOrFail($category->id);
    $attributes = $request->validated();
    $category->update($attributes);

    if ($oldType !== $category->type) {
        $urlParams = $category->type === 'income' ? ['tab' => '2'] : [];

        return HtmxResponse::addFragment('categories.show', 'panel', ['category' => $category])
            ->location(route('categories.index', $urlParams))
            ->retarget('#'.$category->type.'-list')
            ->reswap('afterbegin');
    }

    return HtmxResponse::addFragment('categories.show', 'panel', ['category' => $category])
        ->pushUrl(route('categories.index'))
        ->retarget('this')
        ->reswap('outerHTML');
}
1

Detect type change

Check if the category type was changed during the update.
2

Conditional targeting

If the type changed, retarget the response to a different element and change the URL.
3

Swap strategy

Use different swap strategies (afterbegin vs outerHTML) based on the scenario.

Alpine.js Usage

Alpine.js provides reactive UI components for interactive elements like dropdowns, modals, and form interactions.

Installation

Alpine.js is included in the project’s package.json:
package.json
{
  "devDependencies": {
    "alpinejs": "^3.4.2"
  }
}

Common Patterns

Alpine.js is used for:
  • Tab switching in transaction forms
  • Color picker interactions
  • Modal dialogs
  • Dropdown menus
  • Theme toggling (dark/light mode)
  • Toast notifications

Helper Functions

Cashify includes global helper functions loaded via app/helpers.php:
app/helpers.php
function flashToast($type, $description, $title = null, $position = 'top-right'): void
{
    if (is_null($title)) {
        $title = match ($type) {
            'success' => __('Success'),
            'error' => __('Error'),
            'warning' => __('Warning'),
            'info' => __('Info'),
            default => __('Notification'),
        };
    }

    $toasts = session()->get('toasts', []);
    $toasts[] = [
        'type' => $type,
        'title' => $title,
        'description' => $description,
        'position' => $position
    ];

    session()->flash('toasts', $toasts);
}

function updateNetworth(): void
{
    $netWorth = Account::where('user_id', Auth::id())->sum('balance');

    Auth::user()->netWorth()->create([
        'net_worth' => $netWorth,
    ]);
}

Request Validation

Form requests handle validation logic in app/Http/Requests/:
Validates transaction creation/updates with rules for amount, title, category, and account.

Filters and Query Builders

Cashify uses dedicated filter classes for complex queries:
app/Filters/TransactionFilter.php
class TransactionFilter
{
    protected Request $request;

    public function apply(Builder $query): Builder
    {
        $this->applyTypeFilter($query);
        $this->applyCategoryFilter($query);
        $this->applyAmountFilter($query);
        $this->applyTitleFilter($query);
        $this->applyDetailsFilter($query);
        $this->applyDateRangeFilter($query);

        return $query;
    }

    protected function applyTypeFilter(Builder $query): void
    {
        if ($this->request->filled('types')) {
            $types = $this->request->get('types');

            $query->whereHas('category', function ($q) use ($types) {
                $q->whereIn('type', $types);
            });
        }
    }
}
This pattern keeps controllers clean by extracting query filtering logic into dedicated, testable classes.

Charts and Data Visualization

Cashify uses ApexCharts through dedicated chart builder classes:
app/Charts/SpendingChart.php
class SpendingChart
{
    public function build(): array
    {
        $categories = Auth::user()->categories()->where('type', 'expense')->get();

        $data = [];
        $labels = [];

        foreach ($categories as $category) {
            $totalAmount = abs(Transaction::where('category_id', $category->id)->sum('amount'));
            $data[] = $totalAmount;
            $labels[] = $category->name;
        }

        return [
            'data' => $data,
            'labels' => $labels,
        ];
    }
}

Traits

Reusable functionality is extracted into traits:
app/Traits/HasColor.php
trait HasColor
{
    protected static array $availableColors = [
        'gray', 'orange', 'amber', 'yellow', 'lime', 'green',
        'emerald', 'teal', 'cyan', 'sky', 'blue', 'indigo',
        'violet', 'purple', 'fuchsia', 'pink', 'rose',
    ];

    public function getColorClassAttribute(): string
    {
        $shade = property_exists($this, 'shade') ? $this->shade : 300;
        return 'bg-'.$this->color.'-'.$shade;
    }

    public static function getAvailableColors(): array
    {
        return self::$availableColors;
    }
}
The HasColor trait is used by both Category and Account models to provide consistent color handling across the application.

Build docs developers (and LLMs) love