“Mój Budżet” to aplikacja internetowa stworzona w języku PHP, która umożliwia użytkownikom kompleksowe zarządzanie finansami osobistymi. Aplikacja pozwala na rejestrowanie przychodów i wydatków, kategoryzowanie ich, a następnie analizowanie przepływów pieniężnych za pomocą czytelnych zestawień i wykresów. Dodatkowo, użytkownicy mogą definiować limity wydatków dla poszczególnych kategorii, co wspiera świadome planowanie budżetu.


Struktura Projektu
MOJ_BUDZET/
├── cli.php # Skrypt do inicjalizacji bazy danych
├── composer.json # Definicje zależności i autoloadingu
├── database.sql # Schemat bazy danych
├── README.md
├── public/ # Katalog publicznie dostępny
│ ├── index.php # Główny punkt wejściowy aplikacji
│ └── assets/ # Zasoby front-endowe (CSS, JS, obrazy)
│ ├── balance.css
│ ├── balance.js # Logika strony bilansu, wykresy, DateRangePicker
│ ├── income.js # Logika formularza dodawania przychodu
│ ├── mainStyle.css
│ ├── outcome.js # Logika formularza dodawania wydatku, interakcja z API limitów
│ ├── registerStyle.css
│ ├── settings.css
│ ├── settings.js # Logika strony ustawień, dynamiczne formularze, modale
│ └── welcomeStyle.css
└── src/ # Główny kod źródłowy aplikacji
├── App/ # Logika specyficzna dla "Mój Budżet"
│ ├── Config/ # Konfiguracja (ścieżki, trasy, middleware)
│ │ ├── Middleware.php
│ │ ├── Paths.php
│ │ └── Routes.php
│ ├── Controllers/ # Kontrolery (logika żądań HTTP)
│ │ ├── AboutController.php
│ │ ├── ApiController.php # Obsługa zapytań API (np. limity)
│ │ ├── AuthController.php
│ │ ├── BalanceController.php
│ │ ├── ErrorController.php
│ │ ├── HomeController.php
│ │ ├── SettingsController.php
│ │ └── TransactionController.php
│ ├── Exceptions/ # Własne wyjątki
│ │ └── SessionException.php
│ ├── Middleware/ # Klasy pośredniczące (np. autoryzacja, CSRF)
│ ├── Services/ # Logika biznesowa (operacje na danych)
│ │ ├── ApiService.php
│ │ ├── TransactionService.php
│ │ ├── UserService.php
│ │ └── ValidatorService.php
│ ├── views/ # Szablony widoków(HTML generowany przez PHP)
│ │ ├── errors/
│ │ ├── partials/ # Fragmenty widoków (nagłówek, stopka)
│ │ └── ... (pliki .php dla każdej strony)
│ ├── bootstrap.php # Inicjalizacja aplikacji
│ ├── container-definitions.php # Definicje dla kontenera DI
│ └── functions.php # Funkcje pomocnicze
├── Framework/ # Rdzeń aplikacji (lekki framework PHP)
│ ├── Contracts/ # Interfejsy
│ ├── Exceptions/ # Wyjątki frameworka
│ ├── Rules/ # Reguły walidacji
│ ├── App.php # Główna klasa aplikacji frameworka
│ ├── Container.php # Kontener wstrzykiwania zależności
│ ├── Database.php # Obsługa bazy danych (PDO)
│ ├── Http.php
│ ├── Router.php # System trasowania
│ ├── TemplateEngine.php # Silnik szablonów
│ └── Validator.php # System walidacji
└── vendor/ # Zależności ComposerRdzeń Frameworka
Sercem aplikacji jest lekki framework znajdujący się w katalogu src/Framework/. Dostarcza on kluczowych komponentów:
App.php:
Główna klasa aplikacji, inicjuje router i kontener DI, a także zarządza globalnymi middleware.
// src/Framework/App.php
<?php
declare(strict_types=1);
namespace Framework;
class App
{
private Router $router;
private Container $container;
public function __construct(string $containerDefinitionsPath = null)
{
$this->router = new Router();
$this->container = new Container();
if ($containerDefinitionsPath) {
$containerDefinitions = include $containerDefinitionsPath;
$this->container->addDefinitions($containerDefinitions);
}
}
public function run()
{
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$method = $_SERVER['REQUEST_METHOD'];
$this->router->dispatch($path, $method, $this->container);
}
public function get(string $path, array $controller): App
{
$this->router->add('GET', $path, $controller);
return $this;
}
// ...
}Router.php:
Odpowiada za mapowanie ścieżek URL na odpowiednie akcje kontrolerów. Umożliwia definiowanie tras dla metod GET i POST oraz przypisywanie do nich middleware. Obsługuje również błędy 404, kierując do zdefiniowanego ErrorController.
// src/Framework/Router.php
<?php
declare(strict_types=1);
namespace Framework;
class Router
{
private array $routes = [];
private array $middlewares = [];
private array $errorHandler;
public function add(string $method, string $path, array $controller)
{
$path = $this->normalizePath($path);
$this->routes[] = [
'path' => $path,
'method' => strtoupper($method),
'controller' => $controller,
'middlewares' => []
];
}
public function dispatch(string $path, string $method, Container $container = null)
{
$path = $this->normalizePath($path); // Normalizacja ścieżki
$method = strtoupper($method); // Ujednolicenie metody HTTP
foreach ($this->routes as $route) {
if (
!preg_match("#^{$route['path']}$#", $path, $matches) || // Sprawdzenie dopasowania ścieżki i metody
$route['method'] !== $method
) {
continue;
}
$params = []; // Ekstrakcja parametrów z URL
foreach ($matches as $key => $value) {
if (is_string($key)) {
$params[$key] = $value;
}
}
[$class, $function] = $route['controller']; // Pobranie klasy kontrolera i metody
// Utworzenie instancji kontrolera z pomocą kontenera DI lub bezpośrednio
$controllerInstance = $container ?
$container->resolve($class) :
new $class;
$action = fn () => $controllerInstance->$function(...array_values($params)); // Akcja do wykonania
$allMiddleware = [...$route['middlewares'], ...$this->middlewares]; // Połączenie middleware globalnych i przypisanych do trasy
// Aplikowanie middleware
foreach ($allMiddleware as $middleware) {
$middlewareInstance = $container ?
$container->resolve($middleware) :
new $middleware;
$action = fn () => $middlewareInstance->process($action);
}
$action(); // Wykonanie akcji (z middleware)
return;
}
$this->dispatchNotFound($container); // Obsługa błędu 404
}
// ...
}Container.php:
Prosty kontener wstrzykiwania zależności (DI), który zarządza instancjami klas i ich zależnościami. Rozwiązuje zależności na podstawie definicji lub automatycznie przez refleksję.
// src/Framework/Container.php
<?php
declare(strict_types=1);
namespace Framework;
use ReflectionClass, ReflectionNamedType;
use Framework\Exceptions\ContainerException;
class Container
{
private array $definitions = [];
private array $resolved = [];
public function addDefinitions(array $newDefinitions)
{
$this->definitions = [...$this->definitions, ...$newDefinitions];
}
public function resolve(string $className)
{
$reflectionClass = new ReflectionClass($className);
if (!$reflectionClass->isInstantiable()) {
throw new ContainerException("Class {$className} is not instantiable");
}
$constructor = $reflectionClass->getConstructor();
if (!$constructor) { // Brak konstruktora, zwracamy nową instancję
return new $className;
}
$params = $constructor->getParameters();
if (count($params) === 0) { // Konstruktor bez parametrów
return new $className;
}
$dependencies = []; // Rozwiązywanie zależności konstruktora
foreach ($params as $param) {
$name = $param->getName();
$type = $param->getType();
if (!$type) {
throw new ContainerException("Failed to resolve class {$className} because param {$name} is missing a type hint.");
}
if (!$type instanceof ReflectionNamedType || $type->isBuiltin()) {
throw new ContainerException("Failed to resolve class {$className} because invalid param name.");
}
$dependencies[] = $this->get($type->getName()); // Rekursywne pobieranie zależności
}
return $reflectionClass->newInstanceArgs($dependencies); // Tworzenie instancji z zależnościami
}
public function get(string $id)
{
if (!array_key_exists($id, $this->definitions)) {
throw new ContainerException("Class {$id} does not exist in container.");
}
if (array_key_exists($id, $this->resolved)) { // Zwracanie już utworzonej instancji (singleton)
return $this->resolved[$id];
}
$factory = $this->definitions[$id]; // Pobranie fabryki z definicji
$dependency = $factory($this); // Utworzenie zależności za pomocą fabryki
$this->resolved[$id] = $dependency; // Zapisanie utworzonej instancji
return $dependency;
}
}Database.php:
Warstwa abstrakcji bazy danych, wykorzystująca PDO do komunikacji z bazą MySQL. Umożliwia wykonywanie zapytań przygotowanych i pobieranie wyników.
// src/Framework/Database.php
<?php
declare(strict_types=1);
namespace Framework;
use PDO, PDOException, PDOStatement;
class Database
{
private PDO $connection;
private PDOStatement $stmt;
public function __construct(
string $driver,
array $config,
string $username,
string $password
) {
$config_query = http_build_query(data: $config, arg_separator: ';');
$dsn = "{$driver}:{$config_query}";
try {
$this->connection = new PDO($dsn, $username, $password, [
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC // Domyślny tryb pobierania danych jako tablica asocjacyjna
]);
} catch (PDOException $e) {
die("Unable to connect to database"); // Zakończenie działania w przypadku błędu połączenia
}
}
public function query(string $query, array $params = []): Database
{
$this->stmt = $this->connection->prepare($query); // Przygotowanie zapytania
$this->stmt->execute($params); // Wykonanie zapytania z parametrami
return $this; // Umożliwia łączenie metod (chaining)
}
public function count()
{
return $this->stmt->fetchColumn(); // Pobranie liczby wierszy (np. z COUNT(*))
}
public function fetch()
{
return $this->stmt->fetch(); // Pobranie pojedynczego wiersza
}
public function fetchAll()
{
return $this->stmt->fetchAll(PDO::FETCH_ASSOC); // Pobranie wszystkich wierszy
}
}TemplateEngine.php:
Podstawowy silnik szablonów, który renderuje widoki PHP. Pozwala na przekazywanie danych do widoków oraz dodawanie globalnych zmiennych dostępnych we wszystkich szablonach.
// src/Framework/TemplateEngine.php
<?php
declare(strict_types=1);
namespace Framework;
class TemplateEngine
{
private array $globalTemplateData = [];
public function __construct(private string $basePath) {} // Ścieżka bazowa do widoków
public function render(string $template, array $data = [])
{
extract($data, EXTR_SKIP); // Ekstrakcja danych lokalnych do zmiennych
extract($this->globalTemplateData, EXTR_SKIP); // Ekstrakcja danych globalnych
ob_start(); // Rozpoczęcie buforowania wyjścia
include $this->resolve($template); // Dołączenie pliku szablonu
$output = ob_get_contents(); // Pobranie zawartości bufora
ob_end_clean(); // Zakończenie buforowania i wyczyszczenie bufora
return $output; // Zwrócenie wyrenderowanej treści
}
public function resolve(string $path)
{
return "{$this->basePath}/{$path}"; // Tworzenie pełnej ścieżki do pliku szablonu
}
public function addGlobal(string $key, mixed $value)
{
$this->globalTemplateData[$key] = $value; // Dodawanie danych globalnych
}
}Validator.php:
System walidacji danych, który współpracuje z zestawem reguł (klasy implementujące RuleInterface w katalogu Rules/). Pozwala na definiowanie aliasów dla reguł i walidację danych formularzy.
// src/Framework/Validator.php
<?php
declare(strict_types=1);
namespace Framework;
use Framework\Contracts\RuleInterface;
use Framework\Exceptions\ValidationException;
class Validator
{
private array $rules = []; // Przechowuje zarejestrowane reguły walidacji
public function add(string $alias, RuleInterface $rule) // Dodawanie nowej reguły
{
$this->rules[$alias] = $rule;
}
public function validate(array $formData, array $fields) // Główna metoda walidująca
{
$errors = [];
foreach ($fields as $fieldName => $rulesDefinitions) { // Iteracja po polach do zwalidowania
foreach ($rulesDefinitions as $ruleDefinition) { // Iteracja po regułach dla danego pola
$ruleParams = [];
$ruleName = $ruleDefinition;
if (str_contains($ruleName, ':')) { // Sprawdzenie czy reguła ma parametry
[$ruleName, $ruleParamsString] = explode(':', $ruleName);
$ruleParams = explode(',', $ruleParamsString); // Podział parametrów
}
$ruleValidator = $this->rules[$ruleName]; // Pobranie obiektu walidatora dla danej reguły
if (!$ruleValidator->validate($formData, $fieldName, $ruleParams)) { // Wywołanie walidacji
// Dodanie błędu jeśli walidacja nie powiodła się
$errors[$fieldName][] = $ruleValidator->getMessage(
$formData,
$fieldName,
$ruleParams
);
}
}
}
if (count($errors)) { // Jeśli wystąpiły błędy, rzuć wyjątek
throw new ValidationException($errors);
}
}
}Główne Moduły Aplikacji (Logika w src/App/)
Zarządzanie Transakcjami (TransactionController.php, TransactionService.php):
- Umożliwia dodawanie przychodów i wydatków z walidacją kwoty, daty, kategorii i opcjonalnego komentarza.
// src/App/Services/TransactionService.php - fragment metody createIncome()
public function createIncome(array $formData)
{
$date = $formData['date']; // Formatowanie daty (jeśli potrzeba)
if (strpos($date, '.') !== false) {
list($day, $month, $year) = explode('.', $date);
$date = sprintf('%04d-%02d-%02d', $year, $month, $day);
}
$amount = trim($formData['amount']); // Normalizacja kwoty
$amount = str_replace(',', '.', $amount);
$this->db->query(
'INSERT INTO incomes VALUES (NULL, :user_id, :income_category_assigned_to_user_id, :amount, :date_of_income, :income_comment)',
[
'user_id' => $_SESSION['logged_id'],
'income_category_assigned_to_user_id' => $formData['category'],
'amount' => $amount,
'date_of_income' => $date,
'income_comment' => $formData['comment']
]
);
$_SESSION['income_added'] = true; // Ustawienie flagi sukcesu
}- Kwoty są odpowiednio formatowane przed zapisem do bazy danych.



API (ApiController.php, ApiService.php):
- Aplikacja posiada prosty endpoint API (
GET /api/limit/{id_kategorii}/date/{data}) zwracający dane o limicie dla kategorii wydatków oraz sumie dotychczasowych wydatków w danym miesiącu. Jest on wykorzystywany przez skryptoutcome.jsdo dynamicznego informowania użytkownika.
// src/App/Services/ApiService.php - fragment metody fetchLimit()
public function fetchLimit($categoryId): array|false
{
$categoryIdInt = (int) $categoryId;
$limit = $this->db->query(
'SELECT `limit` FROM expenses_category_assigned_to_users WHERE id = :id',
['id' => $categoryIdInt]
)->fetch();
return $limit;
}
// src/App/Services/ApiService.php - fragment metody fetchMoneyspentSoFar()
public function fetchMoneyspentSoFar($categoryId, $pickedDate): array
{
// ... (logika pobierania sumy wydatków dla kategorii i miesiąca) ...
return $spentSum;
}
Mechanizmy Pośredniczące (Middleware)
Aplikacja wykorzystuje szereg klas middleware do obsługi różnych aspektów żądań HTTP:
- Ochrona CSRF:
CsrfTokenMiddlewaregeneruje tokeny, aCsrfGuardMiddlewareweryfikuje je dla żądań modyfikujących dane. - Zarządzanie Sesją:
SessionMiddlewareinicjuje sesję i konfiguruje jej parametry. - Obsługa Wyjątków Walidacji:
ValidationExceptionMiddlewareprzechwytuje błędy walidacji, zapisuje je w sesji i przekierowuje użytkownika z powrotem do formularza z zachowaniem wprowadzonych danych. - Dane “Flash”:
FlashMiddlewareudostępnia jednorazowe dane (np. błędy walidacji, stare dane formularza) z sesji do widoków. - Dostarczanie Danych do Szablonów:
TemplateDataMiddlewaredodaje globalne dane do wszystkich widoków (np. tytuł strony).IncomesCategoriesMiddlewareiOutcomesCategoriesMiddlewareładują odpowiednie kategorie użytkownika do sesji, aby były dostępne w formularzach. - Obsługa Zakresu Dat Bilansu:
BalanceDateMiddlewarezarządza wybranym przez użytkownika zakresem dat dla strony bilansu, przechowując go w sesji.
Interakcje Front-endowe
Interfejs użytkownika jest wzbogacony o skrypty JavaScript, które poprawiają użyteczność:
- Dynamiczne formularze: Automatyczne ustawianie daty, dynamiczne wyświetlanie informacji o limitach wydatków (
outcome.js), dynamiczne opcje w zależności od wyborów użytkownika w ustawieniach (settings.js). - Interaktywny wybór dat: Na stronie bilansu wykorzystano bibliotekę DateRangePicker (z jQuery i Moment.js) do łatwego wybierania okresu raportowania (
balance.js). - Wizualizacja danych: Skrypt
balance.jsgeneruje wykresy kołowe podsumowujące przychody i wydatki w podziale na kategorie. - Komunikacja AJAX: Wykorzystywana do przesyłania wybranego zakresu dat na stronie bilansu oraz do pobierania informacji o limitach kategorii podczas dodawania wydatku (
balance.js,outcome.js). - Powiadomienia modalne: Bootstrap jest używany do wyświetlania estetycznych powiadomień i potwierdzeń akcji.