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/.
app/Models/Transaction.php
app/Models/Category.php
<? 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' );
}
Detect type change
Check if the category type was changed during the update.
Conditional targeting
If the type changed, retarget the response to a different element and change the URL.
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:
{
"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:
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/:
TransactionRequest
CategoryRequest
AccountRequest
Validates transaction creation/updates with rules for amount, title, category, and account.
Validates category data including name, type, color, and icon.
Validates account creation with name, balance, and color.
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:
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.