Mój budżet

“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 Composer

Rdzeń 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.

PHP
// 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.

PHP
// 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ę.

PHP
// 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.

PHP
// 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.

PHP
// 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.

PHP
// 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.
PHP
// 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 skrypt outcome.js do dynamicznego informowania użytkownika.
PHP
// 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: CsrfTokenMiddleware generuje tokeny, a CsrfGuardMiddleware weryfikuje je dla żądań modyfikujących dane.
  • Zarządzanie Sesją: SessionMiddleware inicjuje sesję i konfiguruje jej parametry.
  • Obsługa Wyjątków Walidacji: ValidationExceptionMiddleware przechwytuje błędy walidacji, zapisuje je w sesji i przekierowuje użytkownika z powrotem do formularza z zachowaniem wprowadzonych danych.
  • Dane “Flash”: FlashMiddleware udostępnia jednorazowe dane (np. błędy walidacji, stare dane formularza) z sesji do widoków.
  • Dostarczanie Danych do Szablonów: TemplateDataMiddleware dodaje globalne dane do wszystkich widoków (np. tytuł strony). IncomesCategoriesMiddleware i OutcomesCategoriesMiddleware ładują odpowiednie kategorie użytkownika do sesji, aby były dostępne w formularzach.
  • Obsługa Zakresu Dat Bilansu: BalanceDateMiddleware zarzą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.js generuje 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.

GITHUB

https://github.com/szymonMCS/MOJ_BUDZET.git

Udostępnij jeśli spodobał Ci się mój projekt