FBERG - TUKE · 2025/2026

Objektové programovanie

doc. Ing. Beáta Stehlíková, PhD.

Osnova predmetu
T01

Informačná hodina a inštalácia

📘 Podmienky absolvovania predmetu a pravidlá pre semester

1. Informácie o predmete a ciele vzdelávania

Výsledky vzdelávania: Študenti získajú základné teoretické vedomosti a praktické zručnosti z objektovo orientovaného programovania (OOP) v prostredí webových technológií. Po úspešnom absolvovaní bude študent schopný samostatne navrhnúť a vytvoriť funkčnú, objektovo orientovanú webovú aplikáciu prepojenú s databázou.

Stručná osnova predmetu

  • Úvod: Základy PHP na webe a práca s MySQL databázou.
  • Paradigma: Rozdiely medzi procedurálnym a objektovým programovaním.
  • Princípy OOP: Pojem a vlastnosti objektu, inštancia triedy, členské premenné a metódy.
  • Pokročilé vlastnosti: Zapuzdrenie, dedenie, polymorfizmus, atribúty viditeľnosti.
  • Abstrakcia: Rozhrania (interface) a abstraktné triedy.
  • Návrhové vzory: Singleton a Factory.
  • Prax: Komplexné využitie OOP v databázovej webovej aplikácii.

2. Harmonogram výučby

  • 1. týždeň: Informácie ku výučbe, inštalácia SW a doplnkov.
  • 8 týždňov: Intenzívna výučba zameraná na stavebné bloky projektu.
  • 3 týždne: Finalizácia aplikácie, ladenie kódu a príprava na odovzdanie.

4. Technické požiadavky

  • Architektúra: min. 5 DB tabuliek.
  • Technológie: PHP (OOP), MySQL, Docker, VS Code.

5. & 6. Záznamy a zabezpečenie

  • Odporúča sa vlastný notebook pre kontinuitu práce.
  • Z hodín sa vyhotovuje AV záznam pre interné potreby.
  • Obhajoba je nahrávaná povinne.
  • Prísny zákaz vlastných záznamov študentmi!

3. Hodnotenie semestra — Klasifikovaný zápočet (100 bodov)

24b
Priebežná práca v Teams
8 týždňov × 3b
Deadline: do začiatku ďalšej hodiny
Penalizácia za omeškanie –50 % týždenne
16b
Písomné testy na papier
5. a 9. týždeň (2×8b)
15 minút, len pero a papier
20b
Finálny kód aplikácie
5 tabuliek, Docker, OOP architektúra
15b
Obhajoba — Vysvetlenie kódu
15b
Obhajoba — Live Coding
10b
Obhajoba — Terminológia OOP
⚠️

Testy prebiehajú 15 minút, len pero a papier. Prísny zákaz AI/PC/Mobilov. Učebňa bude monitorovaná detektormi na kamery a vysielanie. Podvod = 0 bodov.

👉 Úplné podmienky udelenia zápočtu (PDF)
🚀 Inštalácia softvéru a prostredia

Základný balíček projektu

Pre prácu na projekte si stiahnite základný balíček:

📥 Stiahnuť mojprojekt.zip

Obsahuje: Dockerfile, .htaccess, httpd.conf, docker-compose.yml a priečinok www


⚡ Pre skúsených — Docker už používate

  1. Rozbaliť ZIP do pracovného priečinka.
  2. V termináli zadať:
docker-compose up -d --build

Web beží na localhost:8080, DB na localhost:8081.

🛠 Pre začiatočníkov — Nová inštalácia

  1. Nainštalovať Docker Desktop (Windows/Mac) alebo Docker Engine (Linux).
  2. Nainštalovať VS Code + rozšírenie "Docker" a "PHP Extension Pack".
  3. Povoliť WSL2 (ak ste na Windows).
  4. Rozbaliť ZIP a spustiť kontajnery.
🆘

Prvá pomoc: Vidíte chybu "403 Forbidden"?

Stiahnite si Nepriestrelný Dockerfile a nahraďte ním ten pôvodný. Potom spustite:

docker-compose up -d --build

Tento krok vynúti nové nastavenia práv priamo vo vnútri webového servera.

🕹️ Ovládanie a práca v prostredí

Všetky PHP súbory ukladajte do priečinka www v hlavnom adresári projektu.

Linky

Zobrazenie webu http://localhost:8080
Správa DB (phpMyAdmin) http://localhost:8081

Základné príkazy (Terminál)

docker-compose up -d
docker-compose down
T02

Dátový návrh a MySQLi

🗄️ 1. Návrh databázovej schémy

Naša aplikácia: Receptár vegánskych jedál

Počas celého semestra budeme budovať jednu aplikáciu — receptár vegánskych jedál. Dnes navrhneme jej databázovú schému. Minimum je 5 tabuliek — náš model ich má 10, lebo zodpovedá realite a je pripravený na rozšírenie bez straty dát.

Prehľad tabuliek

  • unitsčíselník — merné jednotky (g, ml, ks…) + multiplier pre výpočet kalórií
  • ingredient_categoriesčíselník — kategórie surovín (zelenina, strukoviny, obilniny…)
  • nutrition_typesčíselník — typy výživových hodnôt (kalórie/kcal, bielkoviny/g…)
  • recipe_categoriesčíselník — kategórie receptov (raňajky, polievka, dezert…)
  • cooking_methodsčíselník — spôsob prípravy (varenie, pečenie, raw…)
  • difficultiesčíselník — obtiažnosť receptu (ľahký, stredný, náročný)
  • seasonsčíselník — ročné obdobia pre sezónnosť surovín
  • users — zatiaľ 1 záznam (admin) — pripravené na rozšírenie
  • ingredients — suroviny napojené na kategóriu, sezónu, base_unit a g_per_piece
  • recipeshlavná entita — napojená na users, difficulties a cooking_methods
  • ingredient_nutritionM:N pivot — výživové hodnoty suroviny per 100g
  • recipe_ingredientsM:N pivot — suroviny receptu + množstvo + jednotka
  • recipe_category_mapM:N pivot — recept môže byť v niekoľkých kategóriách naraz
💡
ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 — píšeme pri každej tabuľke. InnoDB garantuje že cudzie kľúče skutočne fungujú ako ochrana — bez neho databáza cudzí kľúč ignoruje a dovolí vložiť neexistujúci id. utf8mb4 zaručí správne zobrazenie diakritiky.
📄 schema.sql
-- =============================================================================
-- 🌱 VEGÁNSKE RECEPTY – Výučbový dátový model
-- =============================================================================
-- Databáza : MySQL 8.x
-- Kódovanie : utf8mb4
-- Reset     : mysql -u root -p vegan_recipes < vegan_recepty.sql
-- =============================================================================
--
-- SCHÉMA
-- =============================================================================
--
--  ČÍSELNÍKY
--  ┌──────────────┐ ┌──────────────────┐ ┌───────────────┐
--  │    units     │ │ingredient_categor│ │ nutrition_type│
--  │──────────────│ │──────────────────│ │───────────────│
--  │ id           │ │ id               │ │ id            │
--  │ name         │ │ name             │ │ name          │
--  │ multiplier   │ └────────┬─────────┘ │ unit          │
--  └──────┬───────┘          │           └───────┬───────┘
--         │                  │                   │
--  ┌──────────────┐ ┌──────────────────┐         │
--  │  difficulties│ │  cooking_methods │         │
--  │──────────────│ │──────────────────│         │
--  │ id           │ │ id               │         │
--  │ name         │ │ name             │         │
--  └──────┬───────┘ └────────┬─────────┘         │
--         │                  │                   │
--  ┌──────────────┐ ┌──────────────────┐         │
--  │recipe_categor│ │    seasons       │         │
--  │──────────────│ │──────────────────│         │
--  │ id           │ │ id               │         │
--  │ name         │ │ name             │         │
--  └──────┬───────┘ └────────┬─────────┘         │
--         │                  │                   │
--  HLAVNÉ ENTITY             │                   │
--                            │                   │
--  ┌──────────────┐          │                   │
--  │    users     │          │                   │
--  │──────────────│          │                   │
--  │ id           │          │                   │
--  │ name         │          │                   │
--  │ email        │          │                   │
--  │ password     │          │                   │
--  └──────┬───────┘          │                   │
--         │                  │                   │
--         │         ┌────────▼─────────┐         │
--         │         │   ingredients    │         │
--         │         │──────────────────│         │
--         │         │ id               │         │
--         │         │ name             │         │
--         │         │ base_unit(g/ml/ks│         │
--         │         │ g_per_piece      │         │
--         │         │ category_id ─────┘(číselník│
--         │         │ season_id        │         │
--         │         └────────┬─────────┘         │
--         │                  │                   │
--         │         ┌────────▼──────────────────▼┐
--         │         │   ingredient_nutrition      │
--         │         │ (ingredient_id, type_id,    │
--         │         │  value per 100g)            │
--         │         └─────────────────────────────┘
--         │
--  ┌──────▼───────────────────────────────────────┐
--  │                   recipes                    │
--  │──────────────────────────────────────────────│
--  │ id, title, description, instructions         │
--  │ servings, prep_time_min, cook_time_min        │
--  │ is_favorite, user_id                         │
--  │ difficulty_id ──────────► difficulties       │
--  │ cooking_method_id ──────► cooking_methods    │
--  └──────┬───────────────────────────────────────┘
--         │
--    ┌────┴──────────────────────┐
--    │                           │
--  ┌─▼─────────────────┐  ┌─────▼──────────────┐
--  │ recipe_ingredients│  │  recipe_category_map│
--  │ (recipe_id,       │  │  (recipe_id,        │
--  │  ingredient_id,   │  │   category_id)      │
--  │  amount,          │  └─────────────────────┘
--  │  unit_id)         │
--  └───────────────────┘
--
--  VIEW: recipe_nutrition
--  → kalórie a makrá na celý recept aj na porciu
--
-- =============================================================================

SET FOREIGN_KEY_CHECKS = 0;
SET NAMES utf8mb4;

-- =============================================================================
-- DROP – v správnom poradí (pivot → závislé → hlavné → číselníky)
-- =============================================================================
DROP TABLE IF EXISTS recipe_category_map;
DROP TABLE IF EXISTS recipe_ingredients;
DROP TABLE IF EXISTS ingredient_nutrition;
DROP TABLE IF EXISTS recipes;
DROP TABLE IF EXISTS ingredients;
DROP TABLE IF EXISTS users;
DROP TABLE IF EXISTS recipe_categories;
DROP TABLE IF EXISTS cooking_methods;
DROP TABLE IF EXISTS difficulties;
DROP TABLE IF EXISTS nutrition_types;
DROP TABLE IF EXISTS ingredient_categories;
DROP TABLE IF EXISTS seasons;
DROP TABLE IF EXISTS units;
DROP VIEW  IF EXISTS recipe_nutrition;

SET FOREIGN_KEY_CHECKS = 1;


-- =============================================================================
-- ČÍSELNÍKY
-- =============================================================================

-- -----------------------------------------------------------------------------
-- units – jednotky merania
-- -----------------------------------------------------------------------------
-- multiplier = koeficient prepočtu na základnú jednotku (g alebo ml)
-- Výpočet kalórií: amount × multiplier / 100 × calories_per_100g
--
-- Príklad:
--   2 lyžice olivového oleja (884 kcal/100g)
--   = 2 × 15 / 100 × 884 = 265 kcal
--
-- Pre base_unit = ks sa multiplier ignoruje,
-- použije sa ingredients.g_per_piece namiesto neho.
-- -----------------------------------------------------------------------------
CREATE TABLE units (
    id          INT UNSIGNED        NOT NULL AUTO_INCREMENT,
    name        VARCHAR(30)         NOT NULL,
    multiplier  DECIMAL(10, 4)      NOT NULL DEFAULT 1
                    COMMENT 'Prepočet na g alebo ml. Pre ks sa ignoruje.',
    CONSTRAINT pk_units         PRIMARY KEY (id),
    CONSTRAINT uq_units_name    UNIQUE (name),
    CONSTRAINT chk_units_mult   CHECK (multiplier > 0)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
  COMMENT='Jednotky merania s koeficientom pre výpočet kalórií';

INSERT INTO units (name, multiplier) VALUES
('g',       1),
('dag',     10),
('kg',      1000),
('ml',      1),
('l',       1000),
('lyžička', 5),
('lyžica',  15),
('šálka',   240),
('ks',      1);


-- -----------------------------------------------------------------------------
-- ingredient_categories – kategórie surovín
-- -----------------------------------------------------------------------------
CREATE TABLE ingredient_categories (
    id      INT UNSIGNED    NOT NULL AUTO_INCREMENT,
    name    VARCHAR(80)     NOT NULL,
    CONSTRAINT pk_ingredient_categories     PRIMARY KEY (id),
    CONSTRAINT uq_ingredient_categories     UNIQUE (name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
  COMMENT='Kategórie surovín: zelenina, ovocie, strukoviny, obilniny...';

INSERT INTO ingredient_categories (name) VALUES
('Zelenina'),
('Ovocie'),
('Strukoviny'),
('Obilniny a múky'),
('Orechy a semienka'),
('Koreniny a bylinky'),
('Oleje a tuky'),
('Rastlinné mlieka a alternatívy'),
('Ostatné');


-- -----------------------------------------------------------------------------
-- nutrition_types – typy nutričných hodnôt
-- -----------------------------------------------------------------------------
CREATE TABLE nutrition_types (
    id      INT UNSIGNED    NOT NULL AUTO_INCREMENT,
    name    VARCHAR(80)     NOT NULL,
    unit    VARCHAR(20)     NOT NULL COMMENT 'Jednotka nutričnej hodnoty: kcal, g...',
    CONSTRAINT pk_nutrition_types   PRIMARY KEY (id),
    CONSTRAINT uq_nutrition_types   UNIQUE (name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
  COMMENT='Typy nutričných hodnôt – pridanie novej = len INSERT, žiadna zmena schémy';

INSERT INTO nutrition_types (name, unit) VALUES
('Kalórie',     'kcal'),
('Bielkoviny',  'g'),
('Sacharidy',   'g'),
('Tuky',        'g'),
('Vláknina',    'g'),
('Cukry',       'g'),
('Soľ',         'g');


-- -----------------------------------------------------------------------------
-- recipe_categories – kategórie receptov
-- -----------------------------------------------------------------------------
CREATE TABLE recipe_categories (
    id      INT UNSIGNED    NOT NULL AUTO_INCREMENT,
    name    VARCHAR(80)     NOT NULL,
    CONSTRAINT pk_recipe_categories     PRIMARY KEY (id),
    CONSTRAINT uq_recipe_categories     UNIQUE (name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
  COMMENT='Kategórie receptov: raňajky, polievka, hlavné jedlo, dezert...';

INSERT INTO recipe_categories (name) VALUES
('Raňajky'),
('Polievka'),
('Hlavné jedlo'),
('Šalát'),
('Dezert'),
('Nápoj a smoothie'),
('Omáčka a dip'),
('Pečivo');


-- -----------------------------------------------------------------------------
-- cooking_methods – spôsoby prípravy
-- -----------------------------------------------------------------------------
CREATE TABLE cooking_methods (
    id      INT UNSIGNED    NOT NULL AUTO_INCREMENT,
    name    VARCHAR(80)     NOT NULL,
    CONSTRAINT pk_cooking_methods   PRIMARY KEY (id),
    CONSTRAINT uq_cooking_methods   UNIQUE (name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
  COMMENT='Spôsob tepelnej alebo inej úpravy receptu';

INSERT INTO cooking_methods (name) VALUES
('Varenie'),
('Pečenie'),
('Dusenie'),
('Restovanie'),
('Grilovanie'),
('Raw – bez úpravy'),
('Mixovanie'),
('Chladenie a mrazenie');


-- -----------------------------------------------------------------------------
-- difficulties – obtiažnosť receptu
-- -----------------------------------------------------------------------------
CREATE TABLE difficulties (
    id      INT UNSIGNED    NOT NULL AUTO_INCREMENT,
    name    VARCHAR(50)     NOT NULL,
    CONSTRAINT pk_difficulties  PRIMARY KEY (id),
    CONSTRAINT uq_difficulties  UNIQUE (name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
  COMMENT='Obtiažnosť prípravy receptu';

INSERT INTO difficulties (name) VALUES
('Ľahký'),
('Stredný'),
('Náročný');


-- -----------------------------------------------------------------------------
-- seasons – ročné obdobia
-- -----------------------------------------------------------------------------
CREATE TABLE seasons (
    id      INT UNSIGNED    NOT NULL AUTO_INCREMENT,
    name    VARCHAR(50)     NOT NULL,
    CONSTRAINT pk_seasons   PRIMARY KEY (id),
    CONSTRAINT uq_seasons   UNIQUE (name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
  COMMENT='Ročné obdobia pre sezónnosť surovín';

INSERT INTO seasons (name) VALUES
('Jar'),
('Leto'),
('Jeseň'),
('Zima'),
('Celý rok');


-- =============================================================================
-- HLAVNÉ ENTITY
-- =============================================================================

-- -----------------------------------------------------------------------------
-- users – používatelia
-- Zatiaľ 1 admin. Pripravené na rozšírenie o registráciu.
-- -----------------------------------------------------------------------------
CREATE TABLE users (
    id          INT UNSIGNED    NOT NULL AUTO_INCREMENT,
    name        VARCHAR(100)    NOT NULL,
    email       VARCHAR(150)    NOT NULL,
    password    VARCHAR(255)    NOT NULL COMMENT 'bcrypt hash – nikdy plain text!',
    created_at  TIMESTAMP       NOT NULL DEFAULT CURRENT_TIMESTAMP,
    CONSTRAINT pk_users         PRIMARY KEY (id),
    CONSTRAINT uq_users_email   UNIQUE (email)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
  COMMENT='Používatelia – zatiaľ 1 admin, pripravené na rozšírenie';

INSERT INTO users (name, email, password) VALUES
('Admin', 'admin@veganrecepty.sk', '$2b$12$HASH_SEM_PRIDE_BCRYPT');


-- -----------------------------------------------------------------------------
-- ingredients – suroviny
-- -----------------------------------------------------------------------------
-- base_unit určuje "rozmer" suroviny:
--   g   → sypné a tuhé (múka, ryža, zelenina...)
--   ml  → tekuté (olej, mlieko, šťava...)
--   ks  → kusové (jablko, vajce, cibuľa...)
--
-- g_per_piece – priemerná hmotnosť 1 kusu v gramoch
--   Použije sa IBA keď base_unit = 'ks'
--   Pre g a ml je NULL
--
-- Výpočet kalórií podľa base_unit:
--   g / ml : amount × unit.multiplier / 100 × nutrition_value
--   ks     : amount × g_per_piece     / 100 × nutrition_value
-- -----------------------------------------------------------------------------
CREATE TABLE ingredients (
    id              INT UNSIGNED        NOT NULL AUTO_INCREMENT,
    name            VARCHAR(100)        NOT NULL,
    base_unit       ENUM('g','ml','ks') NOT NULL COMMENT 'Základná dimenzia suroviny',
    g_per_piece     DECIMAL(8, 2)       NULL
                        COMMENT 'Priemerná hmotnosť 1 kusu v g. Len pre base_unit = ks.',
    category_id     INT UNSIGNED        NOT NULL,
    season_id       INT UNSIGNED        NULL COMMENT 'NULL = sezóna neznáma',
    CONSTRAINT pk_ingredients           PRIMARY KEY (id),
    CONSTRAINT uq_ingredients_name      UNIQUE (name),
    -- g_per_piece je povinné pre ks a zakázané pre g a ml
    CONSTRAINT chk_ingredients_piece    CHECK (
        (base_unit = 'ks' AND g_per_piece IS NOT NULL) OR
        (base_unit != 'ks' AND g_per_piece IS NULL)
    ),
    CONSTRAINT fk_ingredients_category  FOREIGN KEY (category_id)
        REFERENCES ingredient_categories(id),
    CONSTRAINT fk_ingredients_season FOREIGN KEY (season_id)
    REFERENCES seasons(id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
  COMMENT='Suroviny – základ pre výpočet nutričných hodnôt receptov';

INSERT INTO ingredients (name, base_unit, g_per_piece, category_id, season_id) VALUES
-- Zelenina (category_id=1)
('Paradajky',           'ks',   120,    1, 2),
('Červená cibuľa',      'ks',   130,    1, 5),
('Cesnak',              'ks',   5,      1, 5),
('Mrkva',               'ks',   80,     1, 4),
('Špenát',              'g',    NULL,   1, 1),
('Paprika červená',     'ks',   150,    1, 2),
('Brokolica',           'ks',   350,    1, 3),
('Cuketa',              'ks',   250,    1, 2),
-- Ovocie (category_id=2)
('Citrón',              'ks',   100,    2, 5),
('Avokádo',             'ks',   200,    2, 5),
('Banán',               'ks',   120,    2, 5),
-- Strukoviny (category_id=3)
('Červená šošovica',    'g',    NULL,   3, 5),
('Cícer',               'g',    NULL,   3, 5),
('Čierne fazule',       'g',    NULL,   3, 5),
-- Obilniny (category_id=4)
('Quinoa',              'g',    NULL,   4, 5),
('Ovesné vločky',       'g',    NULL,   4, 5),
('Hnedá ryža',          'g',    NULL,   4, 5),
('Celozrnná múka',      'g',    NULL,   4, 5),
-- Orechy a semienka (category_id=5)
('Kešu oriešky',        'g',    NULL,   5, 5),
('Chia semienka',       'g',    NULL,   5, 5),
('Tahini',              'g',    NULL,   5, 5),
-- Koreniny (category_id=6)
('Kurkuma',             'g',    NULL,   6, 5),
('Kari prášok',         'g',    NULL,   6, 5),
('Rímska rasca',        'g',    NULL,   6, 5),
('Čerstvá bazalka',     'g',    NULL,   6, 2),
-- Oleje (category_id=7)
('Olivový olej',        'ml',   NULL,   7, 5),
('Kokosový olej',       'ml',   NULL,   7, 5),
-- Rastlinné mlieka (category_id=8)
('Kokosové mlieko',     'ml',   NULL,   8, 5),
('Mandľové mlieko',     'ml',   NULL,   8, 5),
('Tofu',                'g',    NULL,   8, 5),
-- Ostatné (category_id=9)
('Sójová omáčka',       'ml',   NULL,   9, 5),
('Javorový sirup',      'ml',   NULL,   9, 5),
('Kakao prášok',        'g',    NULL,   9, 5);


-- -----------------------------------------------------------------------------
-- ingredient_nutrition – nutričné hodnoty surovín (pivot M:N)
-- -----------------------------------------------------------------------------
-- Všetky hodnoty sú na 100 g (alebo 100 ml pre tekuté suroviny).
-- Výhoda pivot prístupu: pridanie nového nutričného ukazovateľa
-- = len INSERT do nutrition_types + INSERT sem. Žiadna zmena schémy.
-- -----------------------------------------------------------------------------
CREATE TABLE ingredient_nutrition (
    ingredient_id       INT UNSIGNED    NOT NULL,
    nutrition_type_id   INT UNSIGNED    NOT NULL,
    value               DECIMAL(8, 2)   NOT NULL
                            COMMENT 'Hodnota na 100g alebo 100ml',
    CONSTRAINT pk_ingredient_nutrition      PRIMARY KEY (ingredient_id, nutrition_type_id),
    CONSTRAINT chk_nutrition_value          CHECK (value >= 0),
    CONSTRAINT fk_nutrition_ingredient      FOREIGN KEY (ingredient_id)
        REFERENCES ingredients(id)     ON DELETE CASCADE,
    CONSTRAINT fk_nutrition_type            FOREIGN KEY (nutrition_type_id)
        REFERENCES nutrition_types(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
  COMMENT='Nutričné hodnoty surovín na 100g/100ml – flexibilný pivot';

-- Nutričné hodnoty (nutrition_type_id: 1=kcal, 2=bielk, 3=sachar, 4=tuky, 5=vlák, 6=cukry)
INSERT INTO ingredient_nutrition (ingredient_id, nutrition_type_id, value) VALUES
-- Paradajky (id=1)
(1, 1, 18.00), (1, 2, 0.90), (1, 3, 3.90), (1, 4, 0.20), (1, 5, 1.20), (1, 6, 2.60),
-- Červená cibuľa (id=2)
(2, 1, 40.00), (2, 2, 1.10), (2, 3, 9.30), (2, 4, 0.10), (2, 5, 1.70), (2, 6, 4.20),
-- Cesnak (id=3)
(3, 1, 149.00),(3, 2, 6.40), (3, 3, 33.10),(3, 4, 0.50), (3, 5, 2.10), (3, 6, 1.00),
-- Špenát (id=5)
(5, 1, 23.00), (5, 2, 2.90), (5, 3, 3.60), (5, 4, 0.40), (5, 5, 2.20), (5, 6, 0.40),
-- Avokádo (id=10)
(10,1, 160.00),(10,2, 2.00), (10,3, 9.00), (10,4, 14.70),(10,5, 6.70), (10,6, 0.60),
-- Banán (id=11)
(11,1, 89.00), (11,2, 1.10), (11,3, 23.00),(11,4, 0.30), (11,5, 2.60), (11,6, 12.20),
-- Červená šošovica (id=12)
(12,1, 353.00),(12,2, 25.80),(12,3, 60.10),(12,4, 1.10), (12,5, 10.70),(12,6, 2.00),
-- Cícer (id=13)
(13,1, 364.00),(13,2, 19.30),(13,3, 60.70),(13,4, 6.00), (13,5, 17.40),(13,6, 10.70),
-- Quinoa (id=15)
(15,1, 368.00),(15,2, 14.10),(15,3, 64.20),(15,4, 6.10), (15,5, 7.00), (15,6, 0.00),
-- Ovesné vločky (id=16)
(16,1, 389.00),(16,2, 16.90),(16,3, 66.30),(16,4, 6.90), (16,5, 10.60),(16,6, 1.00),
-- Olivový olej (id=25)
(25,1, 884.00),(25,2, 0.00), (25,3, 0.00), (25,4, 100.00),(25,5, 0.00),(25,6, 0.00),
-- Kokosové mlieko (id=27)
(27,1, 230.00),(27,2, 2.30), (27,3, 5.50), (27,4, 23.80),(27,5, 2.20), (27,6, 3.30),
-- Tofu (id=29)
(29,1, 76.00), (29,2, 8.10), (29,3, 1.90), (29,4, 4.80), (29,5, 0.30), (29,6, 0.50),
-- Kakao prášok (id=32)
(32,1, 228.00),(32,2, 19.60),(32,3, 57.90),(32,4, 13.70),(32,5, 33.20),(32,6, 1.80);


-- -----------------------------------------------------------------------------
-- recipes – recepty
-- -----------------------------------------------------------------------------
CREATE TABLE recipes (
    id                  INT UNSIGNED        NOT NULL AUTO_INCREMENT,
    user_id             INT UNSIGNED        NOT NULL,
    difficulty_id       INT UNSIGNED        NOT NULL,
    cooking_method_id   INT UNSIGNED        NOT NULL,
    title               VARCHAR(200)        NOT NULL,
    description         TEXT                NULL,
    instructions        TEXT                NULL,
    servings            TINYINT UNSIGNED    NOT NULL DEFAULT 2
                            COMMENT 'Počet porcií',
    prep_time_min       SMALLINT UNSIGNED   NULL
                            COMMENT 'Čas prípravy v minútach',
    cook_time_min       SMALLINT UNSIGNED   NULL
                            COMMENT 'Čas varenia v minútach',
    is_favorite         BOOLEAN             NOT NULL DEFAULT FALSE,
    created_at          TIMESTAMP           NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at          TIMESTAMP           NOT NULL DEFAULT CURRENT_TIMESTAMP
                            ON UPDATE CURRENT_TIMESTAMP,
    CONSTRAINT pk_recipes               PRIMARY KEY (id),
    CONSTRAINT chk_recipes_servings     CHECK (servings > 0),
    CONSTRAINT chk_recipes_prep         CHECK (prep_time_min IS NULL OR prep_time_min >= 0),
    CONSTRAINT chk_recipes_cook         CHECK (cook_time_min IS NULL OR cook_time_min >= 0),
    CONSTRAINT fk_recipes_user          FOREIGN KEY (user_id)
        REFERENCES users(id),
    CONSTRAINT fk_recipes_difficulty    FOREIGN KEY (difficulty_id)
        REFERENCES difficulties(id),
    CONSTRAINT fk_recipes_method        FOREIGN KEY (cooking_method_id)
        REFERENCES cooking_methods(id),
    FULLTEXT KEY ft_recipes_title (title)
        COMMENT 'Full-text vyhľadávanie podľa názvu receptu'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
  COMMENT='Recepty – hlavná entita aplikácie';

INSERT INTO recipes (user_id, difficulty_id, cooking_method_id, title, description, instructions, servings, prep_time_min, cook_time_min, is_favorite) VALUES
(1, 1, 1,
 'Krémová paradajková polievka',
 'Jednoduchá a výživná polievka z čerstvých paradajok s kokosovým mliekom.',
 '1. Opraž cibuľu a cesnak na oleji.\n2. Pridaj nakrájané paradajky, soľ a korenie.\n3. Duste 10 minút.\n4. Rozmixuj do hladka.\n5. Vlej kokosové mlieko a prihrej.',
 4, 10, 20, TRUE),

(1, 1, 7,
 'Avokádový toast',
 'Rýchle a sýte raňajky. Krémové avokádo na opečenom chlebe.',
 '1. Opraž chlieb.\n2. Avokádo roztlač vidličkou, dochuť soľou a citrónom.\n3. Nanes na chlieb a podávaj.',
 2, 10, 5, FALSE),

(1, 2, 1,
 'Červená šošovica na karí',
 'Sýte aromatické karí s kokosovým mliekom. Ideálne s ryžou.',
 '1. Opraž cibuľu, cesnak, kari a kurkumu.\n2. Pridaj šošovicu a kokosové mlieko.\n3. Var 20 minút do zmäknutia.\n4. Podávaj s ryžou a koriandrom.',
 4, 15, 25, TRUE),

(1, 2, 7,
 'Buddha bowl s tofu a quinoou',
 'Farebná miska plná bielkovín, zdravých tukov a vlákniny.',
 '1. Uvar quinou.\n2. Marinuj tofu v sójovej omáčke a opečie.\n3. Poukladaj do misky so zeleninou a tahini omáčkou.',
 2, 20, 20, FALSE),

(1, 1, 6,
 'Smoothie z banána a mandľového mlieka',
 'Rýchle raňajky alebo desiata plná energie.',
 '1. Všetky ingrediencie daj do mixéra.\n2. Mixuj 1 minútu do hladka.\n3. Ihneď podávaj.',
 2, 5, 0, FALSE);


-- =============================================================================
-- PIVOT TABUĽKY (M:N väzby)
-- =============================================================================

-- -----------------------------------------------------------------------------
-- recipe_ingredients – suroviny receptu s množstvami
-- -----------------------------------------------------------------------------
-- Výpočet kalórií závisí od base_unit suroviny:
--   base_unit = g alebo ml → amount × unit.multiplier / 100 × nutrition_value
--   base_unit = ks         → amount × ingredient.g_per_piece / 100 × nutrition_value
-- -----------------------------------------------------------------------------
CREATE TABLE recipe_ingredients (
    recipe_id       INT UNSIGNED        NOT NULL,
    ingredient_id   INT UNSIGNED        NOT NULL,
    amount          DECIMAL(8, 2)       NOT NULL,
    unit_id         INT UNSIGNED        NOT NULL,
    note            VARCHAR(255)        NULL
                        COMMENT 'Napr. nakrájané, prelisované, bez kôstky...',
    CONSTRAINT pk_recipe_ingredients    PRIMARY KEY (recipe_id, ingredient_id),
    CONSTRAINT chk_ri_amount           CHECK (amount > 0),
    CONSTRAINT fk_ri_recipe             FOREIGN KEY (recipe_id)
        REFERENCES recipes(id)      ON DELETE CASCADE,
    CONSTRAINT fk_ri_ingredient         FOREIGN KEY (ingredient_id)
        REFERENCES ingredients(id)  ON DELETE RESTRICT,
    CONSTRAINT fk_ri_unit               FOREIGN KEY (unit_id)
        REFERENCES units(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
  COMMENT='Suroviny receptu s množstvami – základ výpočtu kalórií';

-- Recept 1: Paradajková polievka (unit: 1=g, 8=ks, 6=lyžička, 7=lyžica, 4=ml)
INSERT INTO recipe_ingredients (recipe_id, ingredient_id, amount, unit_id, note) VALUES
(1, 1,  4,  9, 'nakrájané'),        -- 4 ks paradajok
(1, 2,  1,  9, 'nakrájaná'),        -- 1 ks cibule
(1, 3,  3,  9, 'prelisovaný'),      -- 3 ks cesnaku
(1, 25, 2,  7, NULL),               -- 2 lyžice olivového oleja
(1, 27, 400,4, NULL);               -- 400 ml kokosového mlieka

-- Recept 2: Avokádový toast
INSERT INTO recipe_ingredients (recipe_id, ingredient_id, amount, unit_id, note) VALUES
(2, 10, 2,  9, 'zrelé'),            -- 2 ks avokáda
(2, 9,  1,  9, NULL),               -- 1 ks citróna
(2, 25, 1,  7, NULL);               -- 1 lyžica olivového oleja

-- Recept 3: Karí so šošovicou
INSERT INTO recipe_ingredients (recipe_id, ingredient_id, amount, unit_id, note) VALUES
(3, 12, 300,1, NULL),               -- 300 g šošovice
(3, 2,  1,  9, 'nakrájaná'),        -- 1 ks cibule
(3, 3,  4,  9, NULL),               -- 4 ks cesnaku
(3, 27, 400,4, NULL),               -- 400 ml kokosového mlieka
(3, 23, 2,  7, NULL),               -- 2 lyžice kari prášku
(3, 22, 1,  6, NULL),               -- 1 lyžička kurkumy
(3, 17, 300,1, 'uvarená'),          -- 300 g ryže
(3, 25, 1,  7, NULL);               -- 1 lyžica kokosového oleja

-- Recept 4: Buddha bowl
INSERT INTO recipe_ingredients (recipe_id, ingredient_id, amount, unit_id, note) VALUES
(4, 15, 150,1, NULL),               -- 150 g quinoy
(4, 29, 200,1, 'nakrájané'),        -- 200 g tofu
(4, 5,  80, 1, NULL),               -- 80 g špenátu
(4, 21, 2,  7, NULL),               -- 2 lyžice tahini
(4, 30, 2,  7, NULL);               -- 2 lyžice sójovej omáčky

-- Recept 5: Smoothie
INSERT INTO recipe_ingredients (recipe_id, ingredient_id, amount, unit_id, note) VALUES
(5, 11, 2,  9, 'zrelé'),            -- 2 ks banánov
(5, 28, 300,4, NULL),               -- 300 ml mandľového mlieka
(5, 20, 1,  7, NULL);               -- 1 lyžica chia semienok


-- -----------------------------------------------------------------------------
-- recipe_category_map – M:N recepty ↔ kategórie receptov
-- -----------------------------------------------------------------------------
CREATE TABLE recipe_category_map (
    recipe_id   INT UNSIGNED    NOT NULL,
    category_id INT UNSIGNED    NOT NULL,
    CONSTRAINT pk_recipe_category_map   PRIMARY KEY (recipe_id, category_id),
    CONSTRAINT fk_rcm_recipe            FOREIGN KEY (recipe_id)
        REFERENCES recipes(id)          ON DELETE CASCADE,
    CONSTRAINT fk_rcm_category          FOREIGN KEY (category_id)
        REFERENCES recipe_categories(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
  COMMENT='M:N väzba receptov a kategórií – jeden recept môže mať viac kategórií';

INSERT INTO recipe_category_map (recipe_id, category_id) VALUES
(1, 2),     -- polievka → Polievka
(2, 1),     -- toast → Raňajky
(2, 3),     -- toast → Hlavné jedlo
(3, 3),     -- karí → Hlavné jedlo
(4, 3),     -- buddha bowl → Hlavné jedlo
(4, 4),     -- buddha bowl → Šalát
(5, 1),     -- smoothie → Raňajky
(5, 6);     -- smoothie → Nápoj a smoothie


-- =============================================================================
-- VIEW – výpočet nutričných hodnôt receptu
-- =============================================================================
-- Vzorec závisí od base_unit suroviny:
--   g / ml : amount × unit.multiplier / 100 × nutrition.value
--   ks     : amount × ingredient.g_per_piece / 100 × nutrition.value
--
-- VIEW vráti hodnoty za celý recept aj prepočítané na 1 porciu.
-- =============================================================================
CREATE VIEW recipe_nutrition AS
SELECT
    r.id                                        AS recipe_id,
    r.title,
    r.servings,
    nt.name                                     AS nutrition_name,
    nt.unit                                     AS nutrition_unit,
    ROUND(SUM(
        CASE i.base_unit
            WHEN 'ks' THEN ri.amount * i.g_per_piece  / 100 * n.value
            ELSE           ri.amount * u.multiplier   / 100 * n.value
        END
    ), 2)                                       AS total_value,
    ROUND(SUM(
        CASE i.base_unit
            WHEN 'ks' THEN ri.amount * i.g_per_piece  / 100 * n.value
            ELSE           ri.amount * u.multiplier   / 100 * n.value
        END
    ) / r.servings, 2)                          AS value_per_serving
FROM recipes r
JOIN recipe_ingredients  ri ON ri.recipe_id      = r.id
JOIN ingredients          i ON i.id              = ri.ingredient_id
JOIN units                u ON u.id              = ri.unit_id
JOIN ingredient_nutrition n ON n.ingredient_id   = i.id
JOIN nutrition_types     nt ON nt.id             = n.nutrition_type_id
GROUP BY r.id, r.title, r.servings, nt.id, nt.name, nt.unit
ORDER BY r.id, nt.id;


-- =============================================================================
-- UKÁŽKOVÉ DOPYTY (odkomentuj podľa potreby)
-- =============================================================================

-- 1. Kalórie receptu na porciu
-- SELECT title, servings, total_value AS total_kcal, value_per_serving AS kcal_per_serving
-- FROM recipe_nutrition WHERE nutrition_name = 'Kalórie';

-- 2. Všetky nutričné hodnoty jedného receptu
-- SELECT nutrition_name, nutrition_unit, total_value, value_per_serving
-- FROM recipe_nutrition WHERE recipe_id = 3;

-- 3. Recepty podľa kategórie
-- SELECT r.id, r.title FROM recipes r
-- JOIN recipe_category_map rcm ON rcm.recipe_id = r.id
-- JOIN recipe_categories rc ON rc.id = rcm.category_id
-- WHERE rc.name = 'Hlavné jedlo';

-- 4. Recepty obsahujúce danú surovinu
-- SELECT DISTINCT r.id, r.title FROM recipes r
-- JOIN recipe_ingredients ri ON ri.recipe_id = r.id
-- JOIN ingredients i ON i.id = ri.ingredient_id
-- WHERE i.name = 'Červená šošovica';

-- 5. Full-text vyhľadávanie receptov
-- SELECT id, title FROM recipes
-- WHERE MATCH(title) AGAINST('paradajková' IN NATURAL LANGUAGE MODE);

-- 6. Obľúbené recepty
-- SELECT id, title, prep_time_min, cook_time_min FROM recipes
-- WHERE is_favorite = TRUE;

-- 7. Sezónne suroviny (napríklad letné)
-- SELECT i.name, ic.name AS kategoria FROM ingredients i
-- JOIN ingredient_categories ic ON ic.id = i.category_id
-- JOIN seasons s ON s.id = i.season_id
-- WHERE s.name = 'Leto';

-- 8. Recepty podľa spôsobu prípravy
-- SELECT r.title, d.name AS obtiznost FROM recipes r
-- JOIN cooking_methods cm ON cm.id = r.cooking_method_id
-- JOIN difficulties d ON d.id = r.difficulty_id
-- WHERE cm.name = 'Varenie';
-- =============================================================================
🧠 2. OOP — základné pojmy

Trieda, objekt, vlastnosti, metódy

Predtým ste pracovali s procedurálnym PHP. Dnes robíme prvý krok k objektovému. Otvorte si W3Schools — PHP OOP Classes and Objects a pozrieme si čo je trieda a ako sa s ňou pracuje.

Štyri pojmy na celý semester

  • Trieda plán / šablóna — napr. mysqli je trieda napísaná v PHP.
  • Objekt konkrétna inštancia triedy — $conn = new mysqli(...)
  • Vlastnosti dáta objektu — napr. $conn->connect_error
  • Metódy funkcie objektu — napr. $conn->query(), $conn->close()

Procedurálne vs. objektové

❌ Procedurálne (takto nie)

proceduralne.php
// Voľné funkcie, globálne premenné
$conn = mysqli_connect("localhost","root","","vegan");
$result = mysqli_query($conn, "SELECT * FROM recipes");
while ($row = mysqli_fetch_assoc($result)) {
    echo $row['title'];
}

✅ Objektové (takto áno)

objektove.php
// Objekt mysqli — voláme metódy na inštancii
$conn = new mysqli("localhost","root","","vegan");
$result = $conn->query("SELECT * FROM recipes");
while ($row = $result->fetch_assoc()) {
    echo $row['title'];
}
💡
Čo robí while ($row = $result->fetch_assoc()) ?

fetch_assoc() je metóda triedy mysqli_result — objektu ktorý vznikol ako výsledok $conn->query(). Zakaždým vráti jeden riadok ako asociatívne pole kde kľúče sú názvy stĺpcov. Keď už žiadny riadok nie je, vráti NULL a cyklus sa zastaví. Podmienka teda robí dve veci naraz — načíta riadok a zároveň skontroluje či niečo prišlo.

📚 3. Triedy mysqli a mysqli_result

Najdôležitejšie metódy a vlastnosti

Keď vytvoríme inštanciu $conn = new mysqli(...) máme k dispozícii dve triedy — mysqli pre prácu s pripojením a mysqli_result pre prácu s výsledkom dotazu.

Trieda mysqli — pripojenie a dotazy

vlastnosti
$conn->connect_error  // chybová správa ak sa pripojenie nepodarilo — inak NULL
$conn->connect_errno  // číslo chyby pri pripojení — 0 ak OK
$conn->error          // chybová správa posledného SQL dotazu
$conn->affected_rows  // počet riadkov ovplyvnených INSERT / UPDATE / DELETE
$conn->insert_id      // AUTO_INCREMENT id posledného INSERT
metódy
$conn->query($sql)               // spustí SQL dotaz — vráti mysqli_result alebo FALSE
$conn->real_escape_string($str)  // ošetrí reťazec pred vložením do SQL
$conn->set_charset('utf8mb4')    // nastaví kódovanie pripojenia
$conn->select_db($name)          // prepne na inú databázu
$conn->close()                   // zatvorí pripojenie

Trieda mysqli_result — výsledok dotazu

Objekt triedy mysqli_result vznikne ako návratová hodnota $conn->query() — nie priamo cez new.

vlastnosti a metódy
$result->num_rows        // počet riadkov vo výsledku (vlastnosť)
$result->fetch_assoc()   // vráti jeden riadok ako asociatívne pole — kľúče = názvy stĺpcov
$result->fetch_all()     // vráti všetky riadky naraz ako 2D pole
$result->free()          // uvoľní pamäť po spracovaní výsledku
💡
Reťazenie tried v praxi:

mysqliquery() → vráti mysqli_resultfetch_assoc() → vráti asociatívne pole s dátami. Tretia trieda mysqli_stmt (prepared statements) príde v T03.

📋 4. CRUD operácie cez MySQLi (objektový štýl)

Pripojenie a práca s receptami cez inštanciu $conn

Pripojenie k databáze

📄 connect.php
<?php
// Vytvoríme inštanciu triedy mysqli — objekt $conn
$conn = new mysqli("db", "root", "root", "vegan");

// Skontrolujeme vlastnosť connect_error
if ($conn->connect_error) {
    die("Chyba pripojenia: " . $conn->connect_error);
}

READ — čítame všetky recepty

📄 index.php
<?php
require_once 'connect.php';

// $conn je teraz k dispozícii

if ($conn->connect_error) {
    die("Chyba pripojenia: " . $conn->connect_error);
}

// query() je metóda triedy mysqli — vráti objekt mysqli_result
$result = $conn->query("SELECT * FROM recipes");

// fetch_assoc() je metóda triedy mysqli_result
while ($row = $result->fetch_assoc()) {
    echo "<h2>" . $row['title'] . "</h2>";
    echo "<p>"  . $row['description'] . "</p>";
}

$conn->close();

CREATE — pridáme nový recept

📄 create.php
<?php
require_once 'connect.php';

// $conn je teraz k dispozícii

if ($conn->connect_error) {
    die("Chyba pripojenia: " . $conn->connect_error);
}

$title       = "Vegánska čokoládová torta";
$description = "Bez vajec, bez mlieka, plná chuti.";

// ⚠️ Bez prepared statements — to príde v T03
$sql = "INSERT INTO recipes (title, description, user_id)
        VALUES ('$title', '$description', 1)";

$conn->query($sql);

// Vlastnosť insert_id — id práve vloženého záznamu
echo "✅ Recept pridaný! ID: " . $conn->insert_id;

$conn->close();
⚠️
Zatiaľ nevkladáme dáta cez prepared statements — to urobíme v T03 pri PDO. Tam uvidíte prečo je to dôležité a čo je SQL injection.
📝 Úloha na ďalší týždeň (3 body)

Čo treba odovzdať do ďalšej hodiny

  • Navrhni dátový model svojej aplikácie — min. 5 tabuliek vrátane aspoň jednej M:N pivot tabuľky. Zdokumentuj vzťahy medzi tabuľkami.
  • Cez phpMyAdmin (localhost:8081) vytvor databázu s názvom podľa svojej aplikácie a vytvor a naplň tabuľky.
  • Vytvor súbor connect.php — pripojenie k tvojej databáze cez inštanciu $conn.
  • Vytvor súbor operacie.php ktorý:
    • načíta connect.php cez require_once
    • vykoná DROP TABLE IF EXISTS a CREATE TABLE pre jednu vybranú tabuľku
    • vloží 5 záznamov naraz jedným INSERT
    • vypíše všetky záznamy vrátane hlavičky tabuľky

📤 Čo odovzdať do Teams

  • Word dokument — dokumentácia návrhu aplikácie: názvy tabuliek, popis stĺpcov a vzťahy medzi tabuľkami (1:N, M:N)
  • connect.php — skript pripojenia k databáze
  • operacie.php — skript so všetkými operáciami
💡
Postup poznáte z minulého predmetu — ak niečo neviete, použite AI. Je dôležité aby ste rozumeli každému riadku kódu ktorý odovzdáte.

Deadline: pred začiatkom T03. Penalizácia za omeškanie –50 % za každý týždeň omeškania.

T03

PDO a Vlastné modely

🔌 1. Čo je PDO a prečo ho používame

PDO — PHP Data Objects

PDO je trieda vstavaná priamo v PHP — nepísali sme ju my, je súčasťou jazyka. Keď napíšeme new PDO(...) robíme presne to isté čo sme robili pri new mysqli(...) — vytvárame objekt ktorý reprezentuje pripojenie k databáze.

MySQLi sme sa učili preto, aby sme pochopili ako PHP s databázou komunikuje. MySQLi je ale „šitá na mieru" len pre MySQL. PDO funguje rovnako aj keby sme zajtra prepli na inú databázu — PostgreSQL, SQLite, čokoľvek. Kód aplikácie sa nezmení.

Tri dôvody prečo profesionáli používajú PDO

  • Prepared statements sú jednoduchšie V MySQLi sme museli písať bind_param("is", $id, $name) a určovať typy. V PDO stačí execute([':id' => $id]) — menej kódu, menej chýb.
  • Prenositeľnosť Ak sa zmení databázový server, zmeníme jeden riadok v DSN. Zvyšok kódu ostane rovnaký.
  • Prehľadné chyby PDO používa výnimky (Exception) — rovnaký mechanizmus ktorý poznáme z OOP. Chyba sa zachytí v catch bloku, žiadne tiché zlyhania.

Tri triedy ktoré budeme používať

  • PDO pripojenie k databáze — obdoba mysqli
  • PDOStatement výsledok dotazu — obdoba mysqli_result
  • PDOException chybová správa — nové, v MySQLi to nebolo ako objekt
📚 2. Trieda PDO — pripojenie a dotazy

Vlastnosti a metódy triedy PDO

Keď vytvoríme $pdo = new PDO(...) máme k dispozícii objekt triedy PDO. Cez tento objekt spúšťame všetky dotazy. Pozrime sa čo trieda ponúka.

Ako vytvoríme objekt PDO — konštruktor

syntax
$pdo = new PDO($dsn, $username, $password, $options);

// $dsn      — Data Source Name — reťazec s adresou a názvom databázy
// $username — používateľské meno pre MySQL
// $password — heslo
// $options  — pole s nastaveniami správania (voliteľné, ale vždy ho píšeme)

Čo je DSN

DSN = Data Source Name. Reťazec ktorý hovorí PDO akú databázu používame a kde beží:

DSN formát pre MySQL
"mysql:host=ADRESA;dbname=DATABAZA;charset=utf8mb4"

// Príklady:
"mysql:host=localhost;dbname=vegan;charset=utf8mb4"   // lokálny server
"mysql:host=db;dbname=vegan;charset=utf8mb4"          // Docker kontajner 'db'

// Keby sme prešli na PostgreSQL — zmeníme len tento riadok:
"pgsql:host=db;dbname=vegan"                          // zvyšok kódu ostane rovnaký

Options — nastavenia správania PDO

$options — tri nastavenia ktoré vždy píšeme
$options = [
    // Ako PDO hlási chyby
    // ERRMODE_EXCEPTION = pri chybe hodí výnimku → skočí do catch bloku
    // Alternatívy (nepoužívame):
    //   ERRMODE_SILENT  — chyby ticho ignoruje (nebezpečné)
    //   ERRMODE_WARNING — vypíše varovanie ale pokračuje
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,

    // Formát v akom fetch() vracia riadky
    // FETCH_ASSOC = asociatívne pole — rovnaký výsledok ako fetch_assoc() v MySQLi
    //   $row['title'], $row['id'], $row['name']...
    // Alternatívy:
    //   FETCH_NUM  — číselné indexy: $row[0], $row[1]...
    //   FETCH_OBJ  — objekt: $row->title, $row->id...
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,

    // Skutočné vs. emulované prepared statements
    // false = PDO pošle prepared statement priamo do MySQL — maximálna bezpečnosť
    // true  = PDO ich emuluje samo v PHP — menej bezpečné, starší spôsob
    PDO::ATTR_EMULATE_PREPARES => false,
];

Metódy triedy PDO

metódy
$pdo->query($sql)       // spustí SQL bez parametrov — vráti PDOStatement
                        // použijeme keď SQL neobsahuje žiadne externé hodnoty

$pdo->prepare($sql)     // pripraví SQL šablónu s placeholdermi — vráti PDOStatement
                        // použijeme vždy keď SQL obsahuje hodnoty od používateľa

$pdo->lastInsertId()    // vráti AUTO_INCREMENT id posledného INSERT
                        // obdoba $conn->insert_id v MySQLi

$pdo->beginTransaction() // začne transakciu — zmeny sa uložia až po commit()
$pdo->commit()           // potvrdí transakciu — uloží zmeny
$pdo->rollBack()         // zruší transakciu — vráti späť všetky zmeny
💡
query() vs prepare() — kedy čo použiť?

query() — SQL je pevne daný, žiadne hodnoty zvonku. Napr. SELECT * FROM difficulties ORDER BY id — vždy rovnaký dotaz.
prepare() — SQL obsahuje hodnotu ktorá prichádza zvonku (z URL, formulára, premennej). Napr. WHERE id = :id:id sa dosadí bezpečne.

⚠️
Čo je SQL Injection a prečo je nebezpečná

SQL Injection je najčastejší útok na webové aplikácie. Útočník nevkladá len hodnotu — vkladá kus SQL kódu.

Predstavte si tento kód:

// ⚠️ NEBEZPEČNÉ — nikdy takto!
$id  = $_GET['id'];   // útočník zadá: 1 OR 1=1
$sql = "SELECT * FROM recipes WHERE id = " . $id;
// Výsledný SQL:  SELECT * FROM recipes WHERE id = 1 OR 1=1
// Podmienka 1=1 je vždy pravdivá → vráti VŠETKY recepty

A teraz to isté s prepare():

// ✅ BEZPEČNÉ — prepared statement
$stmt = $pdo->prepare("SELECT * FROM recipes WHERE id = :id");
$stmt->execute([':id' => $_GET['id']]);
// PDO pošle SQL a hodnotu ODDELENE — databáza ich nikdy nespojí
// Aj keby útočník zadal  1 OR 1=1  — databáza hľadá recept s id = "1 OR 1=1"
// Žiadny nenájde. Útok zlyhá.

Čo môže útočník urobiť cez SQL Injection:

  • získa všetky dáta — heslá, emaily, osobné údaje všetkých používateľov
  • zmaže celú databázu — '; DROP TABLE users; --
  • zmení dáta — prepíše heslá, sumy, čokoľvek
  • prihlási sa bez hesla — ak útok smeruje na prihlasovací formulár

Pravidlo: kdekoľvek do SQL vstupuje hodnota zvonku (URL, formulár, premenná) — vždy prepare() + execute(). Nikdy lepenie reťazcov.

Toto nie je teoretická hrozba. SQL Injection je každý rok v rebríčku OWASP Top 10 — desať najväčších bezpečnostných rizík webu. Niekoľko reálnych prípadov:

  • PlayStation Network (2011) — únik dát 77 miliónov účtov vrátane čísel platobných kariet. Sony muselo vypnúť celú sieť na 23 dní. Pokuta a náklady presiahli 170 miliónov dolárov.
  • LinkedIn (2012) — ukradnutých 6,5 milióna hashovaných hesiel, neskôr sa ukázalo že skutočný počet bol 117 miliónov. Heslá sa objavili na predaj na darknete.
  • Adobe (2013)153 miliónov účtov, zdrojové kódy produktov ako Photoshop a ColdFusion.

Vo všetkých prípadoch — aplikácia lepila hodnoty priamo do SQL. Tri riadky prepare() by to zastavili.

📚 3. Trieda PDOStatement — výsledok dotazu

Vlastnosti a metódy triedy PDOStatement

Objekt triedy PDOStatement vznikne ako návratová hodnota $pdo->query() alebo $pdo->prepare(). Je to obdoba mysqli_result — cez tento objekt načítame výsledky.

metódy PDOStatement
// ── Pre prepared statements ───────────────────────────────
$stmt->execute($params)  // spustí prepared statement, dosadí hodnoty za placeholdery
                         // $params = asociatívne pole [':placeholder' => hodnota]
                         // alebo indexované pole pre ? placeholdery

// ── Načítanie výsledkov ───────────────────────────────────
$stmt->fetch()           // vráti jeden riadok ako asociatívne pole — alebo FALSE
                         // obdoba fetch_assoc() v MySQLi

$stmt->fetchAll()        // vráti VŠETKY riadky naraz ako pole polí
                         // obdoba while(fetch_assoc()) — ale všetko naraz do poľa

$stmt->fetchColumn()     // vráti hodnotu prvého stĺpca prvého riadku
                         // užitočné pre COUNT(*), MAX(id)...

// ── Informácie o výsledku ─────────────────────────────────
$stmt->rowCount()        // počet riadkov ovplyvnených INSERT / UPDATE / DELETE
                         // pre SELECT nie je spoľahlivé — na to použiť count(fetchAll())

Reťazenie v praxi — porovnanie MySQLi a PDO

MySQLi — čo sme robili

MySQLi
// 4 kroky
$stmt = $conn->prepare(
    "SELECT * FROM recipes WHERE id = ?"
);
$stmt->bind_param("i", $id);  // typ povinný
$stmt->execute();
$res = $stmt->get_result();   // extra krok
$row = $res->fetch_assoc();

PDO — nový spôsob

PDO
// 2 kroky
$stmt = $pdo->prepare(
    "SELECT * FROM recipes WHERE id = :id"
);
$stmt->execute([':id' => $id]); // typ nie je potrebný


$row = $stmt->fetch();

Porovnanie metód — rýchla referencia

MySQLi → PDO
// Jeden riadok
$result->fetch_assoc()         →   $stmt->fetch()

// Všetky riadky
while($r = $res->fetch_assoc())  →   $stmt->fetchAll()

// Počet riadkov
$result->num_rows              →   $stmt->rowCount()   // pozri poznámku vyššie

// Posledné INSERT id
$conn->insert_id               →   $pdo->lastInsertId()
⚠️ 4. PDOException — zachytávanie chýb

try / catch a trieda PDOException

PDOException je trieda chybovej správy. Keď PDO narazí na problém (zlé heslo, server nebeží, zlý SQL) — hodí výnimku. To znamená: program okamžite skočí do catch bloku. Ak catch neexistuje, skript spadne s chybou.

try / catch — štruktúra
try {
    // Skús vykonať tento kód
    $pdo = new PDO($dsn, DB_USER, DB_PASS, $options);

    // Ak sa dostaneme sem — všetko prebehlo bez chyby

} catch (PDOException $e) {
    // Sem skočíme ak new PDO() zlyhalo
    // $e je objekt triedy PDOException

    echo $e->getMessage();   // text popisujúci chybu
    echo $e->getCode();      // číslo chyby
}

MySQLi — manuálna kontrola

MySQLi
$conn = new mysqli("db","root","root","vegan");

// Musíme sami skontrolovať vlastnosť
if ($conn->connect_error) {
    die("Chyba: " . $conn->connect_error);
}
// Ak zabudneme if — pokračujeme so zlým objektom

PDO — automatická výnimka

PDO
try {
    $pdo = new PDO($dsn, "root", "root", $options);

    // Sem sa dostaneme len ak spojenie prebehlo OK

} catch (PDOException $e) {
    // PDO samo skočí sem pri akejkoľvek chybe
    die("Chyba: " . $e->getMessage());
}
⚠️
Pozor na produkčné prostredie

$e->getMessage() vypíše adresu servera, názov databázy aj meno používateľa. Počas výučby a vývoja to vypisujeme — vidíme čo sa stalo. V hotovej aplikácii chybu logujeme do súboru a používateľovi zobrazíme len: „Technická chyba, skúste neskôr."

📄 5. connect.php — pripojenie cez PDO

Nový connect.php — nahrádza MySQLi verziu

Tento súbor vytvoríme raz a budeme ho načítavať do každého PHP skriptu cez require_once 'connect.php'. Výsledok: premenná $pdo — objekt triedy PDO — dostupná v celom skripte.

📄 connect.php
<?php
// ── 1. Konštanty — údaje o serveri ────────────────────────
//
//  define('NAZOV', 'hodnota') vytvorí konštantu.
//  Konštanta sa od premennej líši tromi vecami:
//    - nezačína dolárikom
//    - hodnotu nemožno zmeniť počas behu skriptu
//    - platí všade — aj vo vnútri tried a funkcií
//      (premennú by sme museli posielať ako parameter)

define('DB_HOST',    'db');        // názov Docker kontajnera — bez Dockeru: 'localhost'
define('DB_NAME',    'vegan');     // názov databázy — musí existovať v MySQL
define('DB_USER',    'root');      // používateľské meno pre MySQL
define('DB_PASS',    'root');      // heslo — v produkcii nikdy 'root'
define('DB_CHARSET', 'utf8mb4');   // utf8mb4 = plná podpora diakritiky aj emoji

// ── 2. DSN — Data Source Name ─────────────────────────────
//  Reťazec ktorý hovorí PDO: akú databázu, kde beží server,
//  aké kódovanie. Každá databáza má iný formát DSN.
//  Výsledok: "mysql:host=db;dbname=vegan;charset=utf8mb4"

$dsn = "mysql:host=" . DB_HOST . ";dbname=" . DB_NAME . ";charset=" . DB_CHARSET;

// ── 3. Options — nastavenia správania PDO ─────────────────
//  Asociatívne pole: kľúč = čo nastavujeme, hodnota = ako.
//  Tieto tri riadky píšeme vždy — sú to best practices.

$options = [
    PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION, // chyby ako výnimky
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,       // fetch vracia asociatívne pole
    PDO::ATTR_EMULATE_PREPARES   => false,                  // skutočné prepared statements
];

// ── 4. Vytvorenie PDO objektu ─────────────────────────────
//  Na rozdiel od MySQLi — PDO pri chybe HODÍ výnimku.
//  Preto pripojenie zabalíme do try/catch.

try {
    $pdo = new PDO($dsn, DB_USER, DB_PASS, $options);
    // echo "✅ PDO pripojenie úspešné"; // odkomentuj na testovanie

} catch (PDOException $e) {
    // ⚠️ Toto vypíšeme len počas vývoja — nie v produkcii!
    die("❌ Chyba pripojenia: " . $e->getMessage());
}
// Po tomto riadku je $pdo dostupné v celom skripte.
🏗️ 6. Vlastný model — trieda DifficultyModel

Čo je Model a prečo ho píšeme

Model je trieda ktorá sa stará o všetku prácu s databázou pre jednu konkrétnu tabuľku. Bez modelu by sme SQL písali priamo do index.php, list.php, detail.php... rovnaký kód na desiatich miestach. Keď sa zmení tabuľka — meníme na desiatich miestach. S modelom — zmeníme na jednom.

Nový pojem: konštruktor

Konštruktor je špeciálna metóda ktorá sa zavolá automaticky keď vytvoríme objekt cez new. Vždy sa volá __construct — dva podčiarkovníky pred aj za.

konštruktor — ako funguje
// Keď napíšeme toto...
$model = new DifficultyModel($pdo);

// PHP automaticky zavolá toto:
public function __construct(PDO $pdo)
{
    $this->pdo = $pdo;   // uloží $pdo do vlastnosti objektu
}

// $this znamená "tento konkrétny objekt"
// rovnako ako $conn odkazoval na konkrétne MySQLi pripojenie
💡
Prečo konštruktor dostáva $pdo ako parameter?

Model potrebuje pripojenie k databáze. Nepripája sa sám — dostane hotové $pdo zvonku. To sa volá Dependency Injection — závislosť (pripojenie) vložíme zvonku pri vytváraní objektu. Model nevie nič o tom ako bolo pripojenie vytvorené — len ho používa.

Tabuľka difficulties — jednoduchý číselník

tabuľka
-- Len id a name — ideálny základ pre prvý model
┌────┬──────────┐
│ id │ name     │
├────┼──────────┤
│  1 │ Ľahký    │
│  2 │ Stredný  │
│  3 │ Náročný  │
└────┴──────────┘

DifficultyModel.php — kompletný kód

📄 DifficultyModel.php
<?php
class DifficultyModel
{
    // ── Vlastnosť ─────────────────────────────────────────
    //  Uchováva PDO objekt cez ktorý všetky metódy komunikujú s DB.
    //  Typ PDO = sem môže ísť JEDINE objekt triedy PDO, nič iné.
    //  Zatiaľ public — v T04 (Zapuzdrenie) zmeníme na private.
    public PDO $pdo;

    // ── Konštruktor ───────────────────────────────────────
    //  Zavolá sa automaticky pri:  $model = new DifficultyModel($pdo);
    //  Parameter $pdo dostane náš objekt pripojenia z connect.php
    //  a uloží ho do vlastnosti $this->pdo.
    public function __construct(PDO $pdo)
    {
        $this->pdo = $pdo;
    }

    // ── getAll() ─────────────────────────────────────────
    //  Vráti všetky záznamy z tabuľky difficulties.
    //  query() — SQL bez parametrov, žiadne riziko SQL Injection.
    //  fetchAll() — všetky riadky naraz do poľa.
    public function getAll(): array
    {
        $stmt = $this->pdo->query(
            "SELECT id, name FROM difficulties ORDER BY id"
        );
        return $stmt->fetchAll();
        // Vráti:
        // [
        //   ['id' => 1, 'name' => 'Ľahký'],
        //   ['id' => 2, 'name' => 'Stredný'],
        //   ['id' => 3, 'name' => 'Náročný'],
        // ]
    }

    // ── getById() ────────────────────────────────────────
    //  Vráti jeden záznam podľa ID.
    //  prepare() + execute() — ID prichádza zvonku, musíme použiť
    //  prepared statement (ochrana pred SQL Injection).
    //  fetch() — jeden riadok alebo FALSE ak nenájdený.
    public function getById(int $id): array|false
    {
        $stmt = $this->pdo->prepare(
            "SELECT id, name FROM difficulties WHERE id = :id"
        );
        $stmt->execute([':id' => $id]);
        return $stmt->fetch();
    }

    // ── getCount() ───────────────────────────────────────
    //  Vráti počet záznamov v tabuľke.
    //  fetchColumn() — vráti hodnotu prvého stĺpca prvého riadku.
    //  Ideálne pre COUNT(*) ktorý vždy vráti jedno číslo.
    public function getCount(): int
    {
        $stmt = $this->pdo->query(
            "SELECT COUNT(*) FROM difficulties"
        );
        return (int) $stmt->fetchColumn();
        // (int) pretypuje výsledok na celé číslo
    }
}

Ako model použijeme v pouzi_model.php

📄 pouzi_model.php
<?php
require_once 'connect.php';          // vytvorí $pdo
require_once 'DifficultyModel.php';  // načíta triedu

// Vytvoríme objekt — konštruktor dostane $pdo
// PHP zavolá __construct($pdo) automaticky
$model = new DifficultyModel($pdo);

// ── Všetky záznamy ────────────────────────────────────────
$difficulties = $model->getAll();    // vráti pole

echo "<h2>Obtiažnosti (" . $model->getCount() . ")</h2>";

foreach ($difficulties as $row) {
    echo $row['id'] . " — " . $row['name'] . "<br>";
}

// ── Jeden záznam ──────────────────────────────────────────
$one = $model->getById(2);

if ($one) {
    echo "Našiel som: " . $one['name'];
} else {
    echo "Záznam neexistuje.";
}
🔗 7. JOIN — spájanie tabuliek

Prečo JOIN a kedy ho použijeme

V databáze máme dáta rozdelené do viacerých tabuliek — preto sme navrhovali cudzí kľúč a vzťahy 1:N, M:N. Keď chceme dáta z viacerých tabuliek naraz v jednom výsledku, použijeme JOIN.

Príklad: recept má difficulty_id = 2. V tabuľke recipes je len číslo. Aby sme zobrazili „Stredný" namiesto „2" — musíme spojiť recipes s difficulties.

Dva typy JOIN ktoré budeme používať

  • INNER JOIN vráti len záznamy ktoré majú pár v oboch tabuľkách. Ak recept nemá difficulty_id — vo výsledku sa neobjaví.
  • LEFT JOIN vráti všetky záznamy z ľavej tabuľky aj tie bez páru. Kde pár chýba — stĺpce z pravej tabuľky budú NULL.

INNER JOIN — len záznamy s párom

recipes + difficulties
SELECT
    r.id,
    r.title,
    d.name AS difficulty
FROM recipes r
INNER JOIN difficulties d
    ON d.id = r.difficulty_id
ORDER BY r.id;

-- Výsledok: len recepty ktoré majú
-- difficulty_id napojený na existujúci
-- záznam v difficulties.
-- Recept bez difficulty_id sa neobjaví.

LEFT JOIN — všetky recepty

recipes + difficulties
SELECT
    r.id,
    r.title,
    d.name AS difficulty
FROM recipes r
LEFT JOIN difficulties d
    ON d.id = r.difficulty_id
ORDER BY r.id;

-- Výsledok: VŠETKY recepty.
-- Ak recept nemá difficulty_id —
-- stĺpec difficulty bude NULL.
-- Použijeme keď nechceme stratiť dáta.

JOIN v PDO — v metóde modelu

📄 RecipeModel.php — metóda getAllWithDifficulty()
public function getAllWithDifficulty(): array
{
    $stmt = $this->pdo->query(
        "SELECT
             r.id,
             r.title,
             r.servings,
             r.prep_time_min,
             d.name AS difficulty      -- AS = alias, premenujeme stĺpec vo výsledku
         FROM recipes r                -- r = alias pre tabuľku recipes
         INNER JOIN difficulties d     -- d = alias pre tabuľku difficulties
             ON d.id = r.difficulty_id -- podmienka spojenia: cudzí kľúč = primárny kľúč
         ORDER BY r.id"
    );
    return $stmt->fetchAll();
    // Každý riadok výsledku bude:
    // ['id' => 1, 'title' => 'Karí...', 'servings' => 4, 'prep_time_min' => 15, 'difficulty' => 'Stredný']
}
💡
Prečo aliasy r a d?

Obe tabuľky majú stĺpec id a name. Bez aliasov by SQL nevedel ktorý stĺpec myslíme. r.id = stĺpec id z tabuľky recipes, d.name = stĺpec name z tabuľky difficulties. AS difficulty premenuje stĺpec vo výsledku — v PHP potom píšeme $row['difficulty'].

🔒 8. CONSTRAINT a ON DELETE pravidlá

Čo je CONSTRAINT a prečo ho píšeme

CONSTRAINT je obmedzenie (pravidlo) priamo v databáze ktoré chráni integritu dát. Databáza ho vynucuje sama — bez ohľadu na to kto a akým nástrojom dáta mení. PHP kód, phpMyAdmin, priamy SQL príkaz — pravidlo platí vždy.

Bez CONSTRAINT môžeme vložiť difficulty_id = 999 aj keď taká obtiažnosť neexistuje. Databáza to dovolí — máme nekonzistentné dáta. S CONSTRAINT databáza takýto INSERT odmietne s chybou.

Kde sa CONSTRAINT zapisuje — v CREATE TABLE

syntax
CREATE TABLE recipes (
    id            INT UNSIGNED NOT NULL AUTO_INCREMENT,
    difficulty_id INT UNSIGNED NOT NULL,

    CONSTRAINT pk_recipes            PRIMARY KEY (id),
    CONSTRAINT fk_recipes_difficulty FOREIGN KEY (difficulty_id)
        REFERENCES difficulties(id)
        ON DELETE RESTRICT          -- ← pravidlo pre mazanie
) ENGINE=InnoDB;

-- CONSTRAINT meno_constraintu  TYP_CONSTRAINTU  (stĺpec)
-- Meno je ľubovoľné — píšeme ho preto aby sme vedeli
-- čo presne zlyhalo keď databáza hlási chybu.

Tri ON DELETE pravidlá — kedy ktoré použiť

ON DELETE CASCADE — zmaž aj závislé záznamy
-- Príklad: zmažem recept → automaticky sa zmažú aj jeho ingrediencie
CREATE TABLE recipe_ingredients (
    recipe_id     INT UNSIGNED NOT NULL,
    ingredient_id INT UNSIGNED NOT NULL,

    CONSTRAINT fk_ri_recipe FOREIGN KEY (recipe_id)
        REFERENCES recipes(id)
        ON DELETE CASCADE      -- keď zmažem recept, recipe_ingredients sa zmažú sami
);
💡
CASCADE — používame keď závislé záznamy nemajú zmysel bez rodiča. Ingrediencie receptu bez receptu — bezcenné dáta. Nechajme databázu nech ich zmaže sama.
ON DELETE RESTRICT — zakazuje zmazanie ak existujú závislé záznamy
-- Príklad: chcem zmazať obtiažnosť "Ľahký" ale existujú recepty ktoré ju používajú
CREATE TABLE recipes (
    difficulty_id INT UNSIGNED NOT NULL,

    CONSTRAINT fk_recipes_difficulty FOREIGN KEY (difficulty_id)
        REFERENCES difficulties(id)
        ON DELETE RESTRICT     -- databáza odmietne DELETE ak nejaký recept má difficulty_id = toto id
                               -- RESTRICT je DEFAULT — ak ON DELETE vynecháme, platí RESTRICT
);
💡
RESTRICT — používame pre číselníky a referenčné dáta. Nikto by nemal zmazať obtiažnosť kým ju recepty používajú. Databáza nás ochráni pred nehodou.
ON DELETE SET NULL — nastaví NULL keď rodič zmizne
-- Príklad: zmažem sezónu "Leto" → surovina ostane, len season_id sa nastaví na NULL
CREATE TABLE ingredients (
    season_id INT UNSIGNED NULL,   -- ← stĺpec MUSÍ byť NULL-able!

    CONSTRAINT fk_ingredients_season FOREIGN KEY (season_id)
        REFERENCES seasons(id)
        ON DELETE SET NULL   -- keď zmažem sezónu, suroviny ostanú bez sezóny (NULL)
                             -- nie sú zmazané, len stratia väzbu
);
💡
SET NULL — používame keď vzťah je voliteľný. Surovina bez sezóny je stále platná surovina — len nevieme kedy je sezónna. Stĺpec musí byť NULL-able, inak databáza odmietne pravidlo nastaviť.

Rýchla referencia — kedy ktoré pravidlo

rozhodovanie
-- Závislé záznamy nemajú zmysel bez rodiča   →   ON DELETE CASCADE
--   recipe_ingredients bez recipes
--   recipe_category_map bez recipes

-- Číselník, nesmie sa zmazať ak ho niekto používa  →   ON DELETE RESTRICT (default)
--   difficulties mazáme? Nie kým ju recept používa.
--   cooking_methods, recipe_categories — to isté.

-- Vzťah je voliteľný, záznam má zmysel aj bez väzby  →   ON DELETE SET NULL
--   ingredients.season_id — surovina bez sezóny je ok
--   recipes.cooking_method_id — keby bolo voliteľné
📝 Úloha na ďalší týždeň

Čo treba spraviť do T04

  • Vytvor súbor connect.php cez pdo — prepíš svoje MySQLi pripojenie na PDO. Použi rovnaké hodnoty ako v pôvodnom connect.php.
  • Vyber si jeden číselník z tvojej databázy (tabuľka s id a name) a napíš preň vlastný Model podľa vzoru DifficultyModel.php. Model musí mať metódy getAll(), getById() a getCount().
  • Pozri si CREATE TABLE skripty tvojej databázy a navrhni ku každej tabuľke aké ON DELETE pravidlo tam patrí a prečo:
    • Ktoré tabuľky si vyžadujú CASCADE? Prečo?
    • Ktoré si vyžadujú RESTRICT? Prečo?
    • Má niektorá tabuľka voliteľnú väzbu kde by malo zmysel SET NULL?
  • Napíš SQL SELECT dotaz s INNER JOIN pre dve tabuľky z tvojej aplikácie kde má JOIN zmysel (1:N väzba, cudzí kľúč).
  • Napíš ten istý dotaz s LEFT JOIN — porovnaj výsledky. Kedy by bol LEFT JOIN užitočnejší?

📤 Čo odovzdať do Teams

  • connect_pdo.php — PDO pripojenie
  • MojModel.php — vlastná modelová trieda
  • Krátky Word dokument (½ strany) — zdôvodnenie ON DELETE pravidiel pre tvoju databázu + oba SQL dotazy (INNER JOIN a LEFT JOIN)
💡
Môžete používať AI — ale každý riadok kódu ktorý odovzdáte musíte vedieť vysvetliť. V T05 bude TEST kde budete písať bez AI.

Deadline: pred začiatkom T04. Penalizácia za omeškanie –50 % za každý týždeň omeškania.

T04

Zapuzdrenie (Encapsulation)

🔒 1. Prečo private? — problém s public vlastnosťami

Čo je zapuzdrenie

Zapuzdrenie znamená že trieda skryje svoje vnútorné dáta a dovolí k nim prístup len cez metódy ktoré sama kontroluje. Je to rovnaké ako bankomat — vidíš tlačidlá a obrazovku, nie vnútro stroja. Nemôžeš priamo siahnuť do mechanizmu — musíš použiť rozhranie ktoré banka pripravila.

Problém — public vlastnosť

V T03 sme napísali DifficultyModel s public PDO $pdo. To znamená že ktokoľvek kto pracuje na projekte môže omylom urobiť toto:

problém
$model = new DifficultyModel($pdo);

// ⚠️ Toto PHP dovolí — public vlastnosť je prístupná odkiaľkoľvek
$model->pdo = null;       // omylom prepíšeme pripojenie
$model->pdo = $inePdo;    // omylom dosadíme iný objekt

// Výsledok: pri ďalšom volaní metódy model spadne
// a chybu budeme hľadať dlho lebo nie je zrejmé kde nastala
$model->getAll();  // ❌ spadne — ale prečo?

Riešenie — private vlastnosť

Zmeníme public na private — vlastnosť bude viditeľná len vo vnútri triedy. Ak by niekto omylom skúsil vlastnosť prepísať zvonku, PHP okamžite povie kde je problém — chyba sa nájde hneď, nie po hodine ladenia.

💡
private nie je zámok pred útočníkom — je to zábradlie pri schodoch. Uchráni nás pred tým aby sme sami omylom spadli, alebo aby kolega v tíme nevedome prepísal niečo čo prepísať nemá.
viditeľnosť
class DifficultyModel
{
    private PDO $pdo;   // ← zmenené z public na private

    public function __construct(PDO $pdo)
    {
        $this->pdo = $pdo;  // zvnútra triedy — OK
    }
}

$model = new DifficultyModel($pdo);
$model->pdo = null;   // ❌ Error: Cannot access private property DifficultyModel::$pdo
echo $model->pdo;     // ❌ Error: Cannot access private property DifficultyModel::$pdo
// PHP okamžite ukáže kde je chyba — nemusíme hádať

Tri úrovne viditeľnosti

public / protected / private
class DifficultyModel
{
    public    string $name;      // dostupná odkiaľkoľvek — zvonku, v triede, v potomkoch
    protected PDO    $pdo;       // dostupná len v triede a jej potomkoch (dedenie — T05)
    private   int    $callCount; // dostupná len vo vnútri tejto triedy — nikde inde
}
💡
Pravidlo pre prax:

Vlastnosti píšeme vždy private alebo protected — nikdy public.
Metódy ktoré volá vonkajší kód píšeme public — napr. getAll(), getById(), insert().
Pomocné metódy ktoré volá len trieda sama píšeme private — napr. validateName() ktorá overí vstup pred vložením do databázy.

Príklad private pomocnej metódy

validateName() je private — volá ju len trieda sama pred každým INSERT a UPDATE. Zvonku ju nikto nevolá a nikto o nej nemusí vedieť.

private metóda — validateName()
class DifficultyModel
{
    private PDO $pdo;

    // ── private pomocná metóda ────────────────────────────
    // Volá ju len táto trieda — zvonku nie je dostupná
    // Overí či názov nie je prázdny a vráti očistený reťazec
    private function validateName(string $name): string
    {
        $name = trim($name);  // odstráni medzery zo začiatku a konca

        if ($name === '') {
            throw new InvalidArgumentException("Názov nesmie byť prázdny.");
        }

        return $name;
    }

    // ── public metódy volajú validateName() interne ───────
    public function insert(string $name): int
    {
        $name = $this->validateName($name);  // overíme pred INSERT

        $stmt = $this->pdo->prepare(
            "INSERT INTO difficulties (name) VALUES (:name)"
        );
        $stmt->execute([':name' => $name]);
        return (int) $this->pdo->lastInsertId();
    }

    public function update(int $id, string $name): bool
    {
        $name = $this->validateName($name);  // overíme pred UPDATE

        $stmt = $this->pdo->prepare(
            "UPDATE difficulties SET name = :name WHERE id = :id"
        );
        $stmt->execute([':name' => $name, ':id' => $id]);
        return $stmt->rowCount() > 0;
    }
}
🔑 2. Gettery a settery — kontrolovaný prístup

Ako sa dostaneme k private vlastnosti

Keď je vlastnosť private, zvonku sa k nej nedostaneme priamo. Na čítanie použijeme getter, na zápis setter. Setter môže hodnotu pred uložením skontrolovať a odmietnuť zlú hodnotu.

všeobecný zápis — getter a setter
class MojaTrieda
{
    private string $nazov;
    private int    $pocet;

    // ── GETTER — čítanie private vlastnosti ───────────────
    // Názov: get + názov vlastnosti s veľkým začiatočným písmenom
    // Nemá parametre, návratový typ = typ vlastnosti
    public function getNazov(): string
    {
        return $this->nazov;
    }

    // ── SETTER — zápis s validáciou ───────────────────────
    // Názov: set + názov vlastnosti s veľkým začiatočným písmenom
    // Má jeden parameter — novú hodnotu
    // Návratový typ void = nič nevracia
    public function setPocet(int $pocet): void
    {
        if ($pocet < 0) {
            throw new InvalidArgumentException("Počet nesmie byť záporný.");
        }
        $this->pocet = $pocet;
    }
}

// Použitie zvonku:
$obj = new MojaTrieda();
$obj->getNazov();       // ✅ getter — čítanie OK
$obj->setPocet(5);      // ✅ setter — zápis cez metódu OK
$obj->nazov = "hack";   // ❌ Error: Cannot access private property
💡
V našej aplikácii

V DifficultyModel getter ani setter priamo nepotrebujeme — $pdo sa nastavuje raz v konštruktore a nikdy sa nemení zvonku. Getter a setter ukážeme v praxi keď budeme stavať RecipeModel — tam budú vlastnosti ako počet porcií alebo čas prípravy kde validácia v setteri má reálny zmysel.

✨ 3. Magické metódy — __toString, __get, __set

Čo sú magické metódy

Magické metódy sú špeciálne metódy ktoré PHP zavolá automaticky v určitých situáciách. Poznáme ich podľa dvoch podčiarkovníkov na začiatku — __construct je tiež magická metóda.

__toString() — čo sa stane keď vypíšeme objekt

bez __toString()
$model = new DifficultyModel($pdo);
echo $model;  // ❌ Error: Object of class DifficultyModel
              //    could not be converted to string
s __toString() — priamo v našom DifficultyModel
class DifficultyModel
{
    private PDO $pdo;

    // PHP zavolá túto metódu automaticky keď použijeme echo $model
    public function __toString(): string
    {
        return "DifficultyModel | záznamy: " . $this->getCount();
    }
}

$model = new DifficultyModel($pdo);
echo $model;  // ✅ Vypíše: DifficultyModel | záznamy: 3

__get() a __set() — zachytenie prístupu k neexistujúcej vlastnosti

Tieto metódy sa zavolajú keď sa pokúsime čítať alebo zapisovať vlastnosť ktorá neexistuje alebo je private. V našej aplikácii ich nepoužijeme — ale treba ich poznať.

__get() a __set() — všeobecná ukážka
class PrikladovaTrieda
{
    private array $data = [];

    // Zavolá sa keď čítame vlastnosť ktorá neexistuje
    public function __get(string $name): mixed
    {
        return $this->data[$name] ?? null;
    }

    // Zavolá sa keď zapisujeme vlastnosť ktorá neexistuje
    public function __set(string $name, mixed $value): void
    {
        $this->data[$name] = $value;
    }
}

$obj = new PrikladovaTrieda();
$obj->color = "green";    // zavolá __set('color', 'green')
echo $obj->color;         // zavolá __get('color') → vypíše: green
💡
V našej aplikácii používame:

__construct() — vždy, pri každej triede
__toString() — užitočné pre ladenie a výpis
__get() / __set() — len informačne, v praxi radšej explicitné gettery/settery

Deštruktor __destruct()

Opak konštruktora — zavolá sa automaticky keď objekt zanikne (skript skončí, premenná sa prepíše). V našej aplikácii ho nepoužijeme — PDO si spravuje pripojenie samo.

existuje ale nepoužívame
public function __destruct()
{
    // Zavolá sa keď objekt zanikne
    // Napr. na zatvorenie súboru, uvoľnenie zdrojov
    // PDO to robí samo — my tu nič nepíšeme
}
🏗️ 4. DifficultyModel — kompletný s CRUD

Doplníme model o INSERT, UPDATE, DELETE

Model z T03 mal len čítanie. Teraz pridáme zápis — a uvidíme prečo try/catch pri DELETE nie je len pre pripojenie.

📄 DifficultyModel.php — kompletný
<?php
class DifficultyModel
{
    private PDO $pdo;

    public function __construct(PDO $pdo)
    {
        $this->pdo = $pdo;
    }

    public function __toString(): string
    {
        return "DifficultyModel | záznamy: " . $this->getCount();
    }

    // ── READ ──────────────────────────────────────────────

    public function getAll(): array
    {
        $stmt = $this->pdo->query(
            "SELECT id, name FROM difficulties ORDER BY id"
        );
        return $stmt->fetchAll();
    }

    public function getById(int $id): array|false
    {
        $stmt = $this->pdo->prepare(
            "SELECT id, name FROM difficulties WHERE id = :id"
        );
        $stmt->execute([':id' => $id]);
        return $stmt->fetch();
    }

    public function getCount(): int
    {
        $stmt = $this->pdo->query("SELECT COUNT(*) FROM difficulties");
        return (int) $stmt->fetchColumn();
    }

    // Počet receptov pre každú obtiažnosť
    public function countRecipes(): array
    {
        $stmt = $this->pdo->query(
            "SELECT d.id, d.name, COUNT(r.id) AS recipe_count
             FROM difficulties d
             LEFT JOIN recipes r ON r.difficulty_id = d.id
             GROUP BY d.id, d.name
             ORDER BY d.id"
        );
        return $stmt->fetchAll();
        // Vráti:
        // [['id'=>1, 'name'=>'Ľahký',   'recipe_count'=>3],
        //  ['id'=>2, 'name'=>'Stredný', 'recipe_count'=>5], ...]
    }

    // ── INSERT ────────────────────────────────────────────

    public function insert(string $name): int
    {
        $stmt = $this->pdo->prepare(
            "INSERT INTO difficulties (name) VALUES (:name)"
        );
        $stmt->execute([':name' => trim($name)]);
        return (int) $this->pdo->lastInsertId();
    }

    // ── UPDATE ────────────────────────────────────────────

    public function update(int $id, string $name): bool
    {
        $stmt = $this->pdo->prepare(
            "UPDATE difficulties SET name = :name WHERE id = :id"
        );
        $stmt->execute([':name' => trim($name), ':id' => $id]);
        return $stmt->rowCount() > 0;
        // rowCount() = počet zmenených riadkov
        // true ak sa niečo zmenilo, false ak id neexistovalo
    }

    // ── DELETE ────────────────────────────────────────────
    //
    //  POZOR: ak obtiažnosť používa nejaký recept,
    //  databáza hodí výnimku (RESTRICT constraint).
    //  Zachytíme ju v try/catch v ciselniky.php — nie tu.
    //  Model nech hodí výnimku ďalej — volajúci kód sa rozhodne čo s ňou.

    public function delete(int $id): bool
    {
        $stmt = $this->pdo->prepare(
            "DELETE FROM difficulties WHERE id = :id"
        );
        $stmt->execute([':id' => $id]);
        return $stmt->rowCount() > 0;
    }
}
🏗️ 5. Ostatné číselníkové modely

CookingMethodModel, RecipeCategoryModel, IngredientCategoryModel

Všetky tri majú rovnakú štruktúru ako DifficultyModel — mení sa len názov triedy, názov tabuľky a JOIN v countRecipes(). Napíšete ich s AI podľa vzoru vyššie.

CookingMethodModel.php — štruktúra
<?php
class CookingMethodModel
{
    private PDO $pdo;

    public function __construct(PDO $pdo) { $this->pdo = $pdo; }
    public function __toString(): string  { return "CookingMethodModel | záznamy: " . $this->getCount(); }

    public function getAll(): array        { /* SELECT id, name FROM cooking_methods */ }
    public function getById(int $id)       { /* WHERE id = :id */ }
    public function getCount(): int        { /* COUNT(*) FROM cooking_methods */ }
    public function countRecipes(): array  { /* LEFT JOIN recipes ON cooking_method_id */ }
    public function insert(string $name)   { /* INSERT */ }
    public function update(int $id, string $name) { /* UPDATE */ }
    public function delete(int $id)        { /* DELETE */ }
}
💡
Vidíš opakujúci sa kód?

Všetky 4 modely majú rovnaké metódy — píšeme to isté štyrikrát. V T05 to vyriešime dedením — napíšeme BaseModel raz a ostatné z neho zdedia všetko spoločné.

🔐 6. Bezpečnosť — match() a GET parameter

Prečo match() a nie priamo $_GET do SQL

Na správcovskej stránke určíme ktorý číselník zobrazíme cez URL parameter. Nikdy nesmieme GET parameter vložiť priamo do SQL — útočník môže napísať čokoľvek.

❌ Nebezpečné

priamo z URL do SQL
// URL: ?model=users --
$table = $_GET['model'];
$pdo->query("SELECT * FROM " . $table);
// Útočník dostane tabuľku users!

✅ Bezpečné

match() — len povolené hodnoty
// match() povolí len presne definované hodnoty
// čokoľvek iné → default → DifficultyModel
$model = match($_GET['model'] ?? 'difficulty') {
    'difficulty'   => new DifficultyModel($pdo),
    'cooking'      => new CookingMethodModel($pdo),
    'category'     => new RecipeCategoryModel($pdo),
    'ingredient'   => new IngredientCategoryModel($pdo),
    default        => new DifficultyModel($pdo),
};
📋 7. ciselniky.php — správcovská stránka

Jedna stránka pre všetky číselníky

Stránka zobrazí tabuľku so všetkými hodnotami zvoleného číselníka. Každý riadok má linky Upraviť a Zmazať — žiadne písanie, len klikanie.

📄 ciselniky.php
<?php
require_once 'connect.php';
require_once 'DifficultyModel.php';
require_once 'CookingMethodModel.php';
require_once 'RecipeCategoryModel.php';
require_once 'IngredientCategoryModel.php';

// ── 1. Vyberieme model podľa GET parametra ────────────────
$modelKey = $_GET['model'] ?? 'difficulty';

$model = match($modelKey) {
    'difficulty'   => new DifficultyModel($pdo),
    'cooking'      => new CookingMethodModel($pdo),
    'category'     => new RecipeCategoryModel($pdo),
    'ingredient'   => new IngredientCategoryModel($pdo),
    default        => new DifficultyModel($pdo),
};

// ── 2. Spracujeme akciu ───────────────────────────────────
$action  = $_GET['action'] ?? 'list';
$message = '';

if ($action === 'delete' && isset($_GET['id'])) {
    try {
        $model->delete((int) $_GET['id']);
        $message = "✅ Záznam bol zmazaný.";
    } catch (PDOException $e) {
        // RESTRICT constraint — databáza nedovolí zmazanie
        $message = "❌ Nedá sa zmazať — záznam používajú recepty.";
    }
    $action = 'list';  // po akcii zobrazíme zoznam
}

if ($action === 'insert' && isset($_POST['name'])) {
    $model->insert($_POST['name']);
    $message = "✅ Záznam bol pridaný.";
    $action  = 'list';
}

if ($action === 'update' && isset($_POST['name'], $_POST['id'])) {
    $model->update((int) $_POST['id'], $_POST['name']);
    $message = "✅ Záznam bol upravený.";
    $action  = 'list';
}

// ── 3. Načítame dáta ──────────────────────────────────────
$items     = $model->getAll();
$editItem  = null;

if ($action === 'edit' && isset($_GET['id'])) {
    $editItem = $model->getById((int) $_GET['id']);
}
?>

<!-- ── Navigácia medzi číselníkmi ── -->
<nav style="margin-bottom:1rem">
    <a href="?model=difficulty">Obtiažnosti</a> |
    <a href="?model=cooking">Spôsoby prípravy</a> |
    <a href="?model=category">Kategórie receptov</a> |
    <a href="?model=ingredient">Kategórie surovín</a>
</nav>

<?php if ($message): ?>
    <p><?= $message ?></p>
<?php endif; ?>

<!-- ── Formulár na pridanie / úpravu ── -->
<?php if ($action === 'edit' && $editItem): ?>
    <form method="post" action="?model=<?= $modelKey ?>&action=update">
        <input type="hidden" name="id" value="<?= $editItem['id'] ?>">
        <input type="text"   name="name" value="<?= $editItem['name'] ?>">
        <button type="submit">Uložiť</button>
        <a href="?model=<?= $modelKey ?>">Zrušiť</a>
    </form>
<?php else: ?>
    <form method="post" action="?model=<?= $modelKey ?>&action=insert">
        <input type="text" name="name" placeholder="Nová hodnota...">
        <button type="submit">+ Pridať</button>
    </form>
<?php endif; ?>

<!-- ── Tabuľka hodnôt ── -->
<table border="1" cellpadding="6">
    <tr><th>ID</th><th>Názov</th><th>Akcie</th></tr>
    <?php foreach ($items as $item): ?>
    <tr>
        <td><?= $item['id'] ?></td>
        <td><?= $item['name'] ?></td>
        <td>
            <a href="?model=<?= $modelKey ?>&action=edit&id=<?= $item['id'] ?>">✏️ Upraviť</a>
            <a href="?model=<?= $modelKey ?>&action=delete&id=<?= $item['id'] ?>"
               onclick="return confirm('Naozaj zmazať?')">🗑️ Zmazať</a>
        </td>
    </tr>
    <?php endforeach; ?>
</table>
💡
try/catch pri DELETE — v praxi

Keď sa pokúsime zmazať obtiažnosť ktorú používa recept — databáza hodí výnimku kvôli RESTRICT constraint. catch ju zachytí a zobrazí zrozumiteľnú správu. Bez catch by skript spadol s PHP chybou.

👀 8. Preview T05 — BaseModel (príprava)

Čo si pripravíte na T05

Všetky 4 modely majú rovnaké metódy — píšeme to isté štyrikrát. V T05 to vyriešime dedením. Ako prípravu si pozrite tento vzor — v T05 z neho budeme vychádzať.

📄 BaseModel.php — vzor na T05
<?php
// ============================================================
//  BaseModel — spoločný základ pre všetky číselníkové modely
//  T05: DifficultyModel, CookingMethodModel... budú z neho dediť
// ============================================================

class BaseModel
{
    // protected = dostupné v tejto triede AJ v potomkoch
    // private   = dostupné len v tejto triede — potomkovia by ho nevideli
    protected PDO    $pdo;
    protected string $table;   // každý potomok nastaví svoj názov tabuľky

    public function __construct(PDO $pdo)
    {
        $this->pdo = $pdo;
    }

    public function getAll(): array
    {
        $stmt = $this->pdo->query(
            "SELECT id, name FROM {$this->table} ORDER BY id"
        );
        return $stmt->fetchAll();
    }

    public function getById(int $id): array|false
    {
        $stmt = $this->pdo->prepare(
            "SELECT id, name FROM {$this->table} WHERE id = :id"
        );
        $stmt->execute([':id' => $id]);
        return $stmt->fetch();
    }

    public function getCount(): int
    {
        $stmt = $this->pdo->query(
            "SELECT COUNT(*) FROM {$this->table}"
        );
        return (int) $stmt->fetchColumn();
    }

    public function insert(string $name): int
    {
        $stmt = $this->pdo->prepare(
            "INSERT INTO {$this->table} (name) VALUES (:name)"
        );
        $stmt->execute([':name' => trim($name)]);
        return (int) $this->pdo->lastInsertId();
    }

    public function update(int $id, string $name): bool
    {
        $stmt = $this->pdo->prepare(
            "UPDATE {$this->table} SET name = :name WHERE id = :id"
        );
        $stmt->execute([':name' => trim($name), ':id' => $id]);
        return $stmt->rowCount() > 0;
    }

    public function delete(int $id): bool
    {
        $stmt = $this->pdo->prepare(
            "DELETE FROM {$this->table} WHERE id = :id"
        );
        $stmt->execute([':id' => $id]);
        return $stmt->rowCount() > 0;
    }
}

// ── V T05 prepíšeme modely takto: ────────────────────────────
//
// class DifficultyModel extends BaseModel
// {
//     protected string $table = 'difficulties';
//
//     // getAll, getById, getCount, insert, update, delete
//     // sú zdedené z BaseModel — nepíšeme ich znova
//
//     // Pridáme len to čo je špecifické pre tento model:
//     public function countRecipes(): array { ... }
// }
💡
Prečo protected a nie private?

private $pdo — potomok DifficultyModel by ho nevidel.
protected $pdo — potomok ho vidí a môže použiť.
Toto je rozdiel medzi private a protected v praxi.

📝 Úloha na T05

Čo treba spraviť do T05

Na hodine sme pracovali s aplikáciou Vegánske recepty ako ukážkou. Ty robíš svoju vlastnú aplikáciu — úloha je rovnaká ale použiješ svoje vlastné tabuľky a názvy tried.

  • Pozri si svoj dátový model a identifikuj všetky číselníky — tabuľky ktoré majú len id a name a slúžia ako zoznam hodnôt pre iné tabuľky. Pre každý číselník vytvor vlastný Model podľa vzoru DifficultyModel z hodiny — s metódami getAll(), getById(), getCount(), insert(), update(), delete() a countRecipes() (alebo countItems() — podľa toho čo tvoj číselník počíta).
  • Vytvor súbor pouzi_Model.php a demonštruj v ňom všetky metódy svojho modelu: getAll(), getById(), getCount(), countRecipes(), insert(), update(), delete(). Výsledky vypíš do tabuľky alebo prehľadne na obrazovku. Otestuj aj validáciu — skús vložiť prázdny názov.
  • Vytvor správcovskú stránku ciselniky.php podľa vzoru z hodiny — prispôsobenú tvojim modelom a tvojej aplikácii. Každý číselník musí:
    • zobraziť všetky hodnoty v tabuľke
    • umožniť pridanie novej hodnoty
    • umožniť úpravu existujúcej hodnoty
    • zobraziť zrozumiteľnú chybovú správu ak sa hodnota nedá zmazať pretože ju používajú iné záznamy
  • Pozri si BaseModel.php zo sekcie 8 — zamysli sa nad tým čo by zdedil každý tvoj model a čo by si musel dopísať navyše. Nemusíš to programovať — len si to premysli, v T05 to spravíme spolu.

📤 Čo odovzdať do Teams

  • Súbory všetkých tvojich číselníkových modelov — pomenované podľa tvojej aplikácie, minimálne 2 súbory
  • Súbor pouzi_Model.php.
  • ciselniky.php — funkčná správcovská stránka pre tvoju aplikáciu
💡
AI môžeš použiť — ukážkový kód z hodiny jej daj ako vzor a povedz jej aké máš tabuľky. Každý riadok kódu ktorý odovzdáš ale musíš vedieť vysvetliť — v T05 je TEST.

Deadline: pred začiatkom T05. Penalizácia za omeškanie –50 % za každý týždeň omeškania.

T05

TEST 1 & Dedenie

TEST 8b

✏️ Písomný test 1

  • Trvanie: 15 minút, pero + papier, zákaz AI/PC/mobilov.
  • Látka: PHP triedy, OOP syntax, PDO, MySQLi.

Dedenie (Inheritance)

  • Kľúčové slovo extends.
  • Rodičovská trieda vs. potomok — prepisovanie metód.
  • Konštruktor rodiča cez parent::__construct().
T06

Abstraktné triedy

Abstract — povinná implementácia v potomkoch

  • Kľúčové slovo abstract — čo to znamená a prečo sa používa.
  • Abstraktné metódy — povinná implementácia v potomkoch.
  • Rozdiel medzi abstraktnou triedou a bežnou triedou.
  • Praktické použitie: spoločný základ pre modely (napr. BaseModel).
  • Integrácia do projektu — refaktoring existujúcich tried.
T07

Interface a Polymorfizmus

Rozhrania a polymorfné správanie

  • Rozhranie interface — zmluva bez implementácie.
  • Implementácia viacerých interfacov (implements).
  • Rozdiel: abstract class vs. interface.
  • Polymorfizmus — rôzne objekty, rovnaké volanie metódy.
  • Type hinting a dependency injection v PHP.
T08

Návrhové vzory (Design Patterns)

Singleton, Factory a ďalšie

  • Singleton — jediná inštancia triedy (napr. DB spojenie).
  • Factory — továrňová metóda na vytváranie objektov.
  • Prečo návrhové vzory? Opakovateľné a udržateľné riešenia.
  • Implementácia vzorov priamo v projekte.
  • Prehľad ďalších vzorov: Repository, Observer (informačne).
T09

TEST 2 & M:N Logika

TEST 8b

✏️ Písomný test 2

  • Trvanie: 15 minút, pero + papier, zákaz AI/PC/mobilov.
  • Látka: dedenie, abstrakcia, interface, polymorfizmus, vzory.

Many-to-Many vzťahy

  • Prepojovacia (pivot) tabuľka — návrh a implementácia.
  • OOP prístup k M:N: metódy attach(), detach(), sync().
T10

Finalizácia projektu

Dokončenie a príprava na odovzdanie

  • Dokončenie všetkých CRUD operácií v aplikácii.
  • Code review — kontrola OOP architektúry a čistoty kódu.
  • Ladenie chýb (debugging) — nástroje VS Code + Docker logy.
  • Príprava na obhajobu: štruktúra prezentácie kódu.
  • Odovzdanie finálneho kódu (20b): 5 tabuliek, Docker, OOP.
T11 & T12

Odovzdávanie a prezentovanie zadaní

OBHAJOBA

Záverečná obhajoba

  • Vysvetlenie architektúry a kódu aplikácie (15b).
  • Live Coding — úprava/doplnenie funkcie na mieste (15b).
  • Terminológia OOP — správne používanie pojmov (10b).
  • Obhajoba je nahrávaná povinne.
Odporúčaná literatúra