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.
  • Vzorový test na prípravu: 📄 OOP_testy_ukazka.pdf

Dedenie (Inheritance)

  • Kľúčové slovo extends.
  • Rodičovská trieda vs. potomok — prepisovanie metód.
  • Konštruktor rodiča cez parent::__construct().
🔁 1. Prečo dedenie — problém opakujúceho sa kódu

Čo máme z T04

Z minulej hodiny máme 4 modely — každý obsluhuje jeden číselník. Pozrieme sa na ne a uvidíme problém:

problém — opakujúci sa kód
class DifficultyModel
{
    private PDO $pdo;
    public function __construct(PDO $pdo) { $this->pdo = $pdo; }
    public function getAll(): array        { /* SELECT ... FROM difficulties */ }
    public function getById(int $id)       { /* WHERE id = :id */ }
    public function getCount(): int        { /* COUNT(*) FROM difficulties */ }
    public function insert(string $name)   { /* INSERT INTO difficulties */ }
    public function update(int $id, ...)   { /* UPDATE difficulties */ }
    public function delete(int $id)        { /* DELETE FROM difficulties */ }
}

class CookingMethodModel
{
    private PDO $pdo;
    public function __construct(PDO $pdo) { $this->pdo = $pdo; }
    public function getAll(): array        { /* SELECT ... FROM cooking_methods */ }
    public function getById(int $id)       { /* WHERE id = :id */ }
    public function getCount(): int        { /* COUNT(*) FROM cooking_methods */ }
    public function insert(string $name)   { /* INSERT INTO cooking_methods */ }
    public function update(int $id, ...)   { /* UPDATE cooking_methods */ }
    public function delete(int $id)        { /* DELETE FROM cooking_methods */ }
}

// ... a ďalšie 2 modely — rovnaká štruktúra, len iný názov tabuľky
⚠️
Problém: Ak chceme zmeniť napríklad getAll() — musíme to urobiť na 4 miestach. Ľahko zabudneme na jeden model a vznikne chyba ktorú ťažko nájdeme. Dedenie to vyrieši — napíšeme kód raz.
🧬 2. extends — základná syntax

Ako funguje dedenie

Kľúčové slovo extends hovorí že trieda zdedí všetky vlastnosti a metódy rodičovskej triedy. Potomok môže rodičovské metódy použiť, doplniť alebo prepísať.

všeobecná syntax
class Potomok extends Rodic
{
    // Potomok zdedí všetko z Rodic
    // Tu dopíšeme len to čo je špecifické pre Potomok
}
konkrétny príklad
// Rodič — spoločný základ pre všetky číselníkové modely
class BaseModel
{
    protected PDO    $pdo;
    protected string $table;   // každý potomok nastaví svoju tabuľku

    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();
    }

    // ... getById, getCount, insert, update, delete
}

// Potomok — zdedí všetko z BaseModel, nastaví len svoju tabuľku
class DifficultyModel extends BaseModel
{
    protected string $table = 'difficulties';
    // getAll, getById, getCount, insert, update, delete sú zdedené
    // — nepíšeme ich znova
}

class CookingMethodModel extends BaseModel
{
    protected string $table = 'cooking_methods';
    // rovnaké metódy zdedené — nič viac nepíšeme
}
💡
Prečo protected a nie private?
private PDO $pdo — potomok by ho nevidel, nemohol by ho použiť.
protected PDO $pdo — potomok ho vidí a môže použiť.
Toto je praktický rozdiel medzi private a protected.
🔧 3. parent::__construct() — konštruktor rodiča

Kedy potrebujeme parent::__construct()

Keď potomok nemá vlastný konštruktor — PHP zavolá konštruktor rodiča automaticky. Ale keď potomok má vlastný konštruktor — PHP zavolá len jeho a rodičov konštruktor sa nezavolá automaticky. Musíme ho zavolať ručne cez parent::__construct().

všeobecná ukážka — rodič a potomok
class Zviera
{
    protected string $meno;

    public function __construct(string $meno)
    {
        $this->meno = $meno;  // rodič nastaví meno
    }

    public function predstavSa(): string
    {
        return "Volám sa " . $this->meno;
    }
}

// Potomok BEZ vlastného konštruktora — zdedí rodiča automaticky
class Pes extends Zviera
{
    // konštruktor nepíšeme — zavolá sa Zviera::__construct() automaticky
}

$pes = new Pes("Rex");
echo $pes->predstavSa();  // ✅ Volám sa Rex

// ─────────────────────────────────────────────────────────────

// Potomok S vlastným konštruktorom — musí zavolať rodiča
class Macka extends Zviera
{
    private string $farba;

    public function __construct(string $meno, string $farba)
    {
        parent::__construct($meno);  // ← zavolá Zviera::__construct()
        $this->farba = $farba;       // potom nastaví čo Macka potrebuje navyše
    }

    public function predstavSa(): string
    {
        return "Volám sa " . $this->meno . " a som " . $this->farba;
    }
}

$macka = new Macka("Micka", "čierna");
echo $macka->predstavSa();  // ✅ Volám sa Micka a som čierna

// ─────────────────────────────────────────────────────────────

// Čo sa stane keď zabudneme zavolať parent::__construct()
class ZlaMacka extends Zviera
{
    private string $farba;

    public function __construct(string $meno, string $farba)
    {
        // zabudli sme parent::__construct($meno)
        $this->farba = $farba;
    }
}

$zla = new ZlaMacka("Micka", "čierna");
echo $zla->predstavSa();  // ❌ Volám sa   ← $meno nie je nastavené
v našej aplikácii — BaseModel a DifficultyModel
// DifficultyModel nepotrebuje vlastný konštruktor
// — zdedí BaseModel::__construct(PDO $pdo) automaticky
class DifficultyModel extends BaseModel
{
    protected string $table = 'difficulties';
}

// Ak by DifficultyModel potreboval niečo navyše:
class DifficultyModel extends BaseModel
{
    protected string $table = 'difficulties';

    public function __construct(PDO $pdo)
    {
        parent::__construct($pdo);  // ← nastaví $this->pdo
        // tu môžeme pridať čokoľvek navyše
    }
}
⚠️
Pravidlo: Ak píšeš vlastný konštruktor v potomkovi — parent::__construct() musí byť prvý riadok. Inak rodičove vlastnosti nebudú nastavené a kód spadne.
✏️ 4. Doplnenie a prepisovanie metód

Kedy potomok doplní vlastnú metódu

Metóda countRecipes() je iná pre každý model — každý má iný JOIN podľa toho čo jeho číselník počíta. Preto ju nemôžeme dať do BaseModel — každý potomok si ju napíše sám ako vlastnú metódu navyše k tým ktoré zdedil.

doplnenie — potomok pridá vlastnú metódu
class DifficultyModel extends BaseModel
{
    protected string $table = 'difficulties';

    // getAll, getById, getCount, insert, update, delete — zdedené z BaseModel
    // countRecipes() nie je v BaseModel — dopíšeme ju tu

    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();
    }
}

class CookingMethodModel extends BaseModel
{
    protected string $table = 'cooking_methods';

    // Iný JOIN — prepojenie cez cooking_method_id
    public function countRecipes(): array
    {
        $stmt = $this->pdo->query(
            "SELECT m.id, m.name, COUNT(r.id) AS recipe_count
             FROM cooking_methods m
             LEFT JOIN recipes r ON r.cooking_method_id = m.id
             GROUP BY m.id, m.name
             ORDER BY m.id"
        );
        return $stmt->fetchAll();
    }
}
💡
Doplnenie vs prepisovanie:
Doplnenie — potomok pridá metódu ktorú rodič vôbec nemá (countRecipes())
Prepisovanie (override) — potomok prepíše metódu ktorú rodič má — použije sa verzia potomka
prepisovanie (override) — potomok prepíše metódu rodiča
class BaseModel
{
    public function __toString(): string
    {
        return "Model: {$this->table}";
    }
}

class DifficultyModel extends BaseModel
{
    protected string $table = 'difficulties';

    // Prepíšeme __toString() — použije sa táto verzia, nie rodičova
    public function __toString(): string
    {
        return "DifficultyModel | záznamy: " . $this->getCount();
    }
}

$model = new DifficultyModel($pdo);
echo $model;  // ✅ vypíše: DifficultyModel | záznamy: 3
              // — použila sa verzia potomka, nie rodiča
📄 5. BaseModel — kompletný kód

Spoločný základ pre všetky číselníkové modely

📄 BaseModel.php
<?php
class BaseModel
{
    protected PDO    $pdo;
    protected string $table;

    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
    {
        $name = $this->validateName($name);
        $stmt = $this->pdo->prepare(
            "INSERT INTO {$this->table} (name) VALUES (:name)"
        );
        $stmt->execute([':name' => $name]);
        return (int) $this->pdo->lastInsertId();
    }

    public function update(int $id, string $name): bool
    {
        $name = $this->validateName($name);
        $stmt = $this->pdo->prepare(
            "UPDATE {$this->table} SET name = :name WHERE id = :id"
        );
        $stmt->execute([':name' => $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;
    }

    protected function validateName(string $name): string
{
    $name = trim($name);
    if ($name === '') {
        throw new InvalidArgumentException("Názov nesmie byť prázdny.");
    }
    return $name;
}
}
⚠️ InvalidArgumentException — čo je to?

Zabudovaná PHP trieda pre zlý vstup

InvalidArgumentException nie je súčasť PDO — je to zabudovaná PHP trieda výnimky ktorú použijeme keď chceme zastaviť vykonávanie kódu pretože sme dostali zlý vstup.

ako to čítame
throw new InvalidArgumentException("Názov nesmie byť prázdny.");
//    ↑           ↑                      ↑
// vyhoď    vytvor objekt           správa ktorú
// výnimku  tejto zabudovanej       dostaneme
//          PHP triedy              v catch bloku
rozdiel — kto výnimku hodí
// PDOException — hodí ju PDO keď nastane chyba databázy
try {
    $model->delete($id);
} catch (PDOException $e) {
    echo "Chyba databázy: " . $e->getMessage();
}

// InvalidArgumentException — hodíme ju MY keď dostaneme zlý vstup
try {
    $model->insert("   ");   // prázdny názov
} catch (InvalidArgumentException $e) {
    echo "Zlý vstup: " . $e->getMessage();  // "Názov nesmie byť prázdny."
}
📄 6. DifficultyModel po prepísaní na dedenie

Porovnanie — pred a po dedení

❌ Pred — T04 (80+ riadkov)

všetko písané ručne
class DifficultyModel
{
    private PDO $pdo;

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

    public function getAll(): array
    { /* 5 riadkov */ }

    public function getById(int $id)
    { /* 5 riadkov */ }

    public function getCount(): int
    { /* 4 riadky */ }

    public function insert(string $name): int
    { /* 6 riadkov */ }

    public function update(int $id, string $name): bool
    { /* 6 riadkov */ }

    public function delete(int $id): bool
    { /* 5 riadkov */ }

    private function validateName(string $name): string
    { /* 7 riadkov */ }

    public function countRecipes(): array
    { /* 9 riadkov */ }
    // Spolu: ~60 riadkov
}

✅ Po — T05 (10 riadkov)

dedenie z BaseModel
class DifficultyModel extends BaseModel
{
    protected string $table = 'difficulties';

    // getAll, getById, getCount
    // insert, update, delete
    // validateName
    // — všetko zdedené z BaseModel

    // Len to čo je špecifické:
    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();
    }
}
📝 Úloha na T06

Čo treba spraviť do T06

Na hodine sme si ukázali DifficultyModel. Ty prepíšeš modely svojej aplikácie rovnakým spôsobom.

  • Vytvor BaseModel.php — skopíruj kód z hodiny a prispôsob ho svojej aplikácii ak treba.
  • Prepíš všetky svoje číselníkové modely aby dedili z BaseModel — každý model bude mať len protected string $table a metódu countRecipes() (alebo ekvivalent pre tvoju aplikáciu).
  • Otestuj že ciselniky.php stále funguje — zmena na dedenie nesmie rozbíť existujúcu funkcionalitu.

📤 Čo odovzdať do Teams

  • BaseModel.php
  • Všetky prepísané číselníkové modely
  • ciselniky.php — musí stále fungovať
💡
AI môžeš použiť — daj jej ako vzor DifficultyModel z hodiny a povedz jej aké máš tabuľky. Každý riadok musíš vedieť vysvetliť.

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

T06

Abstraktné triedy

Čo sa naučíme

  • Kľúčové slovo abstract — čo to znamená a prečo sa používa.
  • Abstraktná metóda — povinná implementácia v potomkoch.
  • Rozdiel medzi abstraktnou triedou a bežnou triedou.
  • Refaktoring BaseModel — premeníme ho na abstraktnú triedu.
  • Praktické použitie — začíname stavať RecipeModel.
🧱 1. Prečo abstract — problém ktorý rieši

BaseModel nemá zmysel vytvárať samotný

Z T05 máme BaseModel — spoločný základ pre všetky číselníkové modely. Ale čo sa stane ak niekto omylom urobí new BaseModel($pdo)?

problém — BaseModel samotný nedáva zmysel
$model = new BaseModel($pdo);
$model->getAll();
// ❌ Ktorú tabuľku? $table nie je nastavená — kód spadne

BaseModel existuje len ako základ pre potomkov — nikdy ho nechceme vytvoriť priamo. Kľúčové slovo abstract to zabezpečí automaticky — PHP nedovolí vytvoriť objekt abstraktnej triedy.

riešenie — abstract zakazuje priame vytvorenie
abstract class BaseModel
{
    // ...
}

$model = new BaseModel($pdo);
// ❌ Fatal error: Cannot instantiate abstract class BaseModel
// PHP to jednoducho nedovolí — to je presne čo chceme

$model = new DifficultyModel($pdo);
// ✅ DifficultyModel extends BaseModel — to je v poriadku
💡
Zhrnutie: Abstraktná trieda je šablóna — definuje spoločný základ ale sama o sebe nie je kompletná. Existuje len preto aby z nej dedili potomkovia.
📋 2. Abstraktná metóda — povinná implementácia

Čo je abstraktná metóda

Abstraktná metóda je metóda bez tela — rodič povie "každý potomok musí mať túto metódu" ale nenapíše čo má robiť. Každý potomok si ju implementuje sám podľa svojich potrieb. Ak potomok abstraktnú metódu neimplementuje — PHP vyhodí chybu.

všeobecná ukážka — abstraktná metóda
abstract class Tvar
{
    // Abstraktná metóda — len podpis, žiadne telo
    // Každý potomok ju MUSÍ implementovať
    abstract public function plocha(): float;

    // Bežná metóda — zdedená automaticky, nie je povinné prepísať
    public function popis(): string
    {
        return "Tvar s plochou: " . $this->plocha();
    }
}

class Kruh extends Tvar
{
    private float $polomer;        // 1. deklarujeme vlastnosť

    public function __construct(float $polomer)
    {
        $this->polomer = $polomer; // 2. konštruktor ju nastaví pri new Kruh()
    }


    // POVINNÉ — implementujeme abstraktnú metódu
    public function plocha(): float
    {
        return M_PI * $this->polomer ** 2;
    }
}

class Stvorec extends Tvar
{
   // Konštruktor uloží stranu do vlastnosti — použijeme ju v plocha()
    // private float $strana je skrátený zápis pre: private float $strana; + $this->strana = $strana;
    public function __construct(private float $strana) {}

    // POVINNÉ — každý potomok má vlastnú implementáciu
    public function plocha(): float
    {
        return $this->strana ** 2;
    }
}

$kruh    = new Kruh(5);
$stvorec = new Stvorec(4);

echo $kruh->popis();    // Tvar s plochou: 78.54
echo $stvorec->popis(); // Tvar s plochou: 16
⚠️
Pravidlá abstraktnej triedy:
Abstraktná trieda môže mať bežné metódy aj abstraktné metódy.
Abstraktná trieda nemôže byť priamo vytvorená cez new.
Potomok musí implementovať všetky abstraktné metódy — inak PHP vyhodí chybu.
💡
PHP 8 — skrátený zápis vlastnosti v konštruktore:
public function __construct(private float $strana) {} je skrátený zápis pre:
private float $strana;

public function __construct(float $strana)
{
    $this->strana = $strana;
}
Oba zápisy robia to isté — PHP 8 len umožňuje napísať to na jednom riadku. V projekte používame dlhší zápis — je zrozumiteľnejší pre začiatočníkov.
🔧 3. Refaktoring BaseModel — pridáme abstract

BaseModel sa stane abstraktnou triedou

Pridáme abstraktnú metódu describe() — každý model musí vedieť opísať sám seba. Táto metóda nahrádza __toString() ktorú sme robili v T04 — tentoraz je jej implementácia povinná.

📄 BaseModel.php — po refaktoringu
<?php
abstract class BaseModel
{
    protected PDO    $pdo;
    protected string $table;

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

    // Abstraktná metóda — každý potomok ju MUSÍ implementovať
    abstract public function describe(): string;

    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
    {
        $name = $this->validateName($name);
        $stmt = $this->pdo->prepare(
            "INSERT INTO {$this->table} (name) VALUES (:name)"
        );
        $stmt->execute([':name' => $name]);
        return (int) $this->pdo->lastInsertId();
    }

    public function update(int $id, string $name): bool
    {
        $name = $this->validateName($name);
        $stmt = $this->pdo->prepare(
            "UPDATE {$this->table} SET name = :name WHERE id = :id"
        );
        $stmt->execute([':name' => $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;
    }

    protected function validateName(string $name): string
    {
        $name = trim($name);
        if ($name === '') {
            throw new InvalidArgumentException("Názov nesmie byť prázdny.");
        }
        return $name;
    }
}

Číselníkové modely — doplníme describe()

Každý existujúci číselníkový model musíme doplniť o metódu describe(). Ostatný kód zostáva rovnaký — menšia zmena, ale PHP bude vyžadovať jej prítomnosť.

DifficultyModel.php — doplnená describe()
class DifficultyModel extends BaseModel
{
    protected string $table = 'difficulties';

    // Povinná implementácia abstraktnej metódy
    public function describe(): string
    {
        return "DifficultyModel | tabuľka: {$this->table} | záznamov: " . $this->getCount();
    }

    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();
    }
}
🍽️ 4. RecipeModel — začíname stavať hlavný model

Prečo RecipeModel nemôže len zdediť BaseModel

Číselníkové modely mali jednoduchú štruktúru — tabuľka s id a name. Tabuľka recipes je iná — má viac stĺpcov a prepojenia na číselníky. getAll() z BaseModel vráti len id, name — to nestačí. RecipeModel si musí napísať vlastné metódy.

štruktúra tabuľky recipes
recipes
  id, title, description, instructions,
  prep_time, cook_time, servings,
  difficulty_id    → JOIN difficulties
  cooking_method_id → JOIN cooking_methods
  season_id        → JOIN seasons (môže byť NULL)
📄 RecipeModel.php
<?php
class RecipeModel extends BaseModel
{
    protected string $table = 'recipes';

    // Povinná implementácia abstraktnej metódy
    public function describe(): string
    {
        return "RecipeModel | tabuľka: {$this->table} | receptov: " . $this->getCount();
    }

    // Prepíšeme getAll() — BaseModel vráti len id, name — to nestačí
    public function getAll(): array
    {
        $stmt = $this->pdo->query(
            "SELECT r.id, r.title, r.prep_time, r.cook_time, r.servings,
                    d.name AS difficulty
             FROM recipes r
             LEFT JOIN difficulties d ON d.id = r.difficulty_id
             ORDER BY r.title"
        );
        return $stmt->fetchAll();
    }

    // Prepíšeme getById() — detail receptu so všetkými JOINmi
    public function getById(int $id): array|false
    {
        $stmt = $this->pdo->prepare(
            "SELECT r.*,
                    d.name  AS difficulty_name,
                    m.name  AS cooking_method_name,
                    s.name  AS season_name
             FROM recipes r
             LEFT JOIN difficulties     d ON d.id = r.difficulty_id
             LEFT JOIN cooking_methods  m ON m.id = r.cooking_method_id
             LEFT JOIN seasons          s ON s.id = r.season_id
             WHERE r.id = :id"
        );
        $stmt->execute([':id' => $id]);
        return $stmt->fetch();
    }

    // insert() a update() — vlastné lebo recipes má viac stĺpcov ako len name
    public function insert(array $data): int
    {
        $stmt = $this->pdo->prepare(
            "INSERT INTO recipes
                (title, description, instructions, prep_time, cook_time,
                 servings, difficulty_id, cooking_method_id, season_id)
             VALUES
                (:title, :description, :instructions, :prep_time, :cook_time,
                 :servings, :difficulty_id, :cooking_method_id, :season_id)"
        );
        $stmt->execute($data);
        return (int) $this->pdo->lastInsertId();
    }

    public function update(int $id, array $data): bool
    {
        $data[':id'] = $id;
        $stmt = $this->pdo->prepare(
            "UPDATE recipes SET
                title             = :title,
                description       = :description,
                instructions      = :instructions,
                prep_time         = :prep_time,
                cook_time         = :cook_time,
                servings          = :servings,
                difficulty_id     = :difficulty_id,
                cooking_method_id = :cooking_method_id,
                season_id         = :season_id
             WHERE id = :id"
        );
        $stmt->execute($data);
        return $stmt->rowCount() > 0;
    }

    // delete() zdedíme z BaseModel — funguje rovnako
}
💡
Čo sme prepisali a prečo:
getAll() — BaseModel vracia len id, name, recept má viac stĺpcov
getById() — potrebujeme JOINy na číselníky
insert() — prijíma pole array $data, nie jeden reťazec
update() — rovnaký dôvod ako insert
delete() — zdedené, funguje rovnako pre všetky tabuľky
▶️ 5. Ako RecipeModel použijeme

Základný výpis receptov

📄 recepty.php — výpis zoznamu
<?php
require 'connect.php';
require 'models/BaseModel.php';
require 'models/RecipeModel.php';

$model   = new RecipeModel($pdo);
$recepty = $model->getAll();
?>

<!DOCTYPE html>
<html lang="sk">
<head>
    <meta charset="UTF-8">
    <title>Recepty</title>
</head>
<body>
    <h1>Zoznam receptov</h1>
    <p><?= $model->describe() ?></p>

    <table>
        <tr>
            <th>Názov</th>
            <th>Príprava</th>
            <th>Varenie</th>
            <th>Porcie</th>
            <th>Obtiažnosť</th>
        </tr>
        <?php foreach ($recepty as $recept): ?>
        <tr>
            <td><a href="detail.php?id=<?= $recept['id'] ?>">
                <?= htmlspecialchars($recept['title']) ?>
            </a></td>
            <td><?= $recept['prep_time'] ?> min</td>
            <td><?= $recept['cook_time'] ?> min</td>
            <td><?= $recept['servings'] ?></td>
            <td><?= htmlspecialchars($recept['difficulty']) ?></td>
        </tr>
        <?php endforeach; ?>
    </table>
</body>
</html>
📝 Úloha na T07

Čo treba spraviť do T07

Na hodine sme spolu spravili refaktoring BaseModel a základný model pre hlavnú entitu. Tvojou úlohou je urobiť to isté pre svoju aplikáciu:

  • Doplň describe() do všetkých svojich číselníkových modelov — každý musí implementovať abstraktnú metódu.
  • Vytvor model pre svoju hlavnú entitu — napríklad ProductModel, ArticleModel, BookModel — podľa toho čo je jadro tvojej aplikácie:
    • Dedí z BaseModel
    • Implementuje describe()
    • Vlastný getAll() — JOIN na číselníky ktoré tvoja entita používa
    • Vlastný getById() — detail so všetkými JOINmi
    • Vlastný insert() a update() — tvoja hlavná tabuľka má viac stĺpcov ako len name
  • Ak máš dve hlavné entity — vytvor model pre obe.
  • Vytvor základnú stránku výpisu — podľa ukážky z hodiny — kde použiješ nový model a vypíšeš záznamy z databázy.
💡
Použi model z hodiny ako vzor — štruktúra je rovnaká, líšia sa len stĺpce a JOINy. Daj AI štruktúru svojej hlavnej tabuľky a požiadaj ho aby model vytvoril podľa vzoru z hodiny. Každý riadok musíš vedieť vysvetliť.

📤 Čo odovzdať do Teams

  • BaseModel.php — s abstract a metódou describe()
  • Všetky číselníkové modely — doplnená describe()
  • Model pre hlavnú entitu (príp. dve ak ich máš)
  • Základná stránka výpisu — funkčná, zobrazuje dáta z databázy

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

T07

Interface a Polymorfizmus

Čo sa naučíme

  • Refaktoring BaseModel — pridáme abstraktnú metódu countRelated().
  • Refaktoring RecipeModel a IngredientModel — samostatné triedy bez dedenia.
  • Rozhranie interface — zmluva bez implementácie.
  • Rozdiel medzi abstract class a interface.
  • Polymorfizmus — rôzne objekty, rovnaké volanie metódy.
  • Type hinting — PHP overí typ parametra automaticky.
🔧 1. Refaktoring BaseModel — countRelated()

Prečo countRelated() namiesto countRecipes()

Z T06 majú číselníkové modely metódu countRecipes() — ale nie všetky číselníky patria receptom. IngredientCategoryModel počíta ingrediencie, nie recepty. Preto použijeme všeobecný názov countRelated() — každý model si ho implementuje podľa svojej tabuľky.

📄 BaseModel.php — pridáme abstraktnú metódu
abstract class BaseModel
{
    protected PDO    $pdo;
    protected string $table;

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

    // Povinná implementácia — každý číselník má iný JOIN
    abstract public function describe(): string;
    abstract public function countRelated(): array;

    // ... getAll, getById, getCount, insert, update, delete, validateName
}
číselníky receptov — countRelated() cez LEFT JOIN recipes
class DifficultyModel extends BaseModel
{
    protected string $table = 'difficulties';

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

    // Počíta recepty — číselník patrí receptom
    public function countRelated(): array
    {
        $stmt = $this->pdo->query(
            "SELECT d.id, d.name, COUNT(r.id) AS related_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();
    }
}

// CookingMethodModel, RecipeCategoryModel — rovnaká štruktúra,
// len iný JOIN stĺpec
číselníky ingrediencií — countRelated() cez LEFT JOIN ingredients
class IngredientCategoryModel extends BaseModel
{
    protected string $table = 'ingredient_categories';

    public function describe(): string
    {
        return "IngredientCategoryModel | záznamov: " . $this->getCount();
    }

    // Počíta ingrediencie — tento číselník patrí ingredienciám, nie receptom
    public function countRelated(): array
    {
        $stmt = $this->pdo->query(
            "SELECT c.id, c.name, COUNT(i.id) AS related_count
             FROM ingredient_categories c
             LEFT JOIN ingredients i ON i.category_id = c.id
             GROUP BY c.id, c.name
             ORDER BY c.id"
        );
        return $stmt->fetchAll();
    }
}
🔧 2. Refaktoring RecipeModel — samostatná trieda

Prečo RecipeModel nebude dediť z BaseModel

V T06 sme RecipeModel nechali dediť z BaseModel ale prepisoval skoro všetky metódy — getAll(), getById(), insert(), update(). Keď potomok prepíše všetko — dedenie nepomáha, len komplikuje. RecipeModel bude teraz samostatná trieda.

💡
Pravidlo: Dedenie použijeme len keď naozaj zdieľame kód. Ak potomok prepíše všetko — dedenie nepomáha.
📄 RecipeModel.php — samostatná trieda
<?php
class RecipeModel
{
    private PDO $pdo;

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

    public function describe(): string
    {
        return "RecipeModel | receptov: " . $this->getCount();
    }

    public function getAll(): array
    {
        $stmt = $this->pdo->query(
            "SELECT r.id, r.title, r.prep_time, r.cook_time, r.servings,
                    d.name AS difficulty
             FROM recipes r
             LEFT JOIN difficulties d ON d.id = r.difficulty_id
             ORDER BY r.title"
        );
        return $stmt->fetchAll();
    }

    public function getById(int $id): array|false
    {
        $stmt = $this->pdo->prepare(
            "SELECT r.*,
                    d.name AS difficulty_name,
                    m.name AS cooking_method_name,
                    s.name AS season_name
             FROM recipes r
             LEFT JOIN difficulties    d ON d.id = r.difficulty_id
             LEFT JOIN cooking_methods m ON m.id = r.cooking_method_id
             LEFT JOIN seasons         s ON s.id = r.season_id
             WHERE r.id = :id"
        );
        $stmt->execute([':id' => $id]);
        return $stmt->fetch();
    }

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

    public function insert(array $data): int
    {
        $stmt = $this->pdo->prepare(
            "INSERT INTO recipes
                (title, description, instructions, prep_time, cook_time,
                 servings, difficulty_id, cooking_method_id, season_id)
             VALUES
                (:title, :description, :instructions, :prep_time, :cook_time,
                 :servings, :difficulty_id, :cooking_method_id, :season_id)"
        );
        $stmt->execute($data);
        return (int) $this->pdo->lastInsertId();
    }

    public function update(int $id, array $data): bool
    {
        $data[':id'] = $id;
        $stmt = $this->pdo->prepare(
            "UPDATE recipes SET
                title             = :title,
                description       = :description,
                instructions      = :instructions,
                prep_time         = :prep_time,
                cook_time         = :cook_time,
                servings          = :servings,
                difficulty_id     = :difficulty_id,
                cooking_method_id = :cooking_method_id,
                season_id         = :season_id
             WHERE id = :id"
        );
        $stmt->execute($data);
        return $stmt->rowCount() > 0;
    }

    public function delete(int $id): bool
    {
        $stmt = $this->pdo->prepare("DELETE FROM recipes WHERE id = :id");
        $stmt->execute([':id' => $id]);
        return $stmt->rowCount() > 0;
    }
}
📋 3. Interface — zmluva bez implementácie

Čo je interface

Interface je zmluva — definuje čo trieda musí vedieť, ale nehovorí ako to má urobiť. Žiadny kód, len podpisy metód. Trieda ktorá interface implementuje musí mať všetky jeho metódy — inak PHP vyhodí chybu.

všeobecná ukážka — interface
// Interface — len zmluva, žiadny kód
interface Platba
{
    public function zaplat(float $suma): bool;
    public function vratPlatbu(float $suma): bool;
}

// Trieda musí implementovať VŠETKY metódy z interface
class KartaPlatba implements Platba
{
    public function zaplat(float $suma): bool
    {
        // platba kartou
        return true;
    }

    public function vratPlatbu(float $suma): bool
    {
        // vrátenie na kartu
        return true;
    }
}

class PayPalPlatba implements Platba
{
    public function zaplat(float $suma): bool
    {
        // platba cez PayPal
        return true;
    }

    public function vratPlatbu(float $suma): bool
    {
        // vrátenie cez PayPal
        return true;
    }
}
💡
Rozdiel: abstract class vs interface
abstract class interface
Môže mať kód v metódach Len podpisy metód — žiadny kód
Môže mať vlastnosti Nemôže mať vlastnosti
Trieda môže dediť len z jednej Trieda môže implementovať viac
Použiť keď zdieľame kód Použiť keď definujeme zmluvu
📄 4. ModelInterface — zmluva pre všetky modely

Spoločná zmluva pre číselníky aj hlavné entity

Všetky naše modely — číselníky aj RecipeModel — majú rovnaké metódy: getAll(), getById(), getCount(), insert(), update(), delete(). Interface to zaručí — PHP overí že každý model má všetky metódy.

📄 ModelInterface.php
<?php
interface ModelInterface
{
    public function getAll(): array;
    public function getById(int $id): array|false;
    public function getCount(): int;
    public function delete(int $id): bool;
    public function describe(): string;
}
BaseModel implementuje interface — číselníky ho zdedia
abstract class BaseModel implements ModelInterface
{
    // BaseModel implementuje ModelInterface
    // Všetky číselníkové modely ho zdedia automaticky
    // — DifficultyModel, CookingMethodModel...
}
RecipeModel implementuje interface samostatne
class RecipeModel implements ModelInterface
{
    // RecipeModel nededí z BaseModel
    // ale implementuje rovnaký interface
    // — zaručuje rovnaké metódy ako číselníky
}
🔀 5. Polymorfizmus — rôzne objekty, rovnaké volanie

Jeden kód funguje s akýmkoľvek modelom

Polymorfizmus znamená že môžeme písať kód ktorý pracuje s akýmkoľvek objektom implementujúcim daný interface — bez toho aby sme vedeli čo je to za triedu. PHP overí typ automaticky cez type hinting.

type hinting — PHP overí typ parametra
// Funkcia prijme AKÝKOĽVEK objekt ktorý implementuje ModelInterface
function vypisZoznam(ModelInterface $model): void
{
    echo "<p>" . $model->describe() . "</p>";
    echo "<ul>";
    foreach ($model->getAll() as $zaznam) {
        echo "<li>" . htmlspecialchars($zaznam['name'] ?? $zaznam['title']) . "</li>";
    }
    echo "</ul>";
}

// Funguje pre VŠETKY modely — lebo všetky implementujú ModelInterface
vypisZoznam(new DifficultyModel($pdo));       // číselník
vypisZoznam(new CookingMethodModel($pdo));    // číselník
vypisZoznam(new IngredientCategoryModel($pdo)); // číselník ingrediencií
vypisZoznam(new RecipeModel($pdo));           // hlavná entita

// Ak by sme poslali objekt bez ModelInterface — PHP vyhodí chybu
// vypisZoznam("nejaky retazec");  ❌ TypeError
praktické použitie — ciselniky.php s polymorfizmom
<?php
require 'connect.php';
require 'interfaces/ModelInterface.php';
require 'models/BaseModel.php';
require 'models/DifficultyModel.php';
require 'models/CookingMethodModel.php';
require 'models/RecipeCategoryModel.php';
require 'models/IngredientCategoryModel.php';

// match() vyberie správny model — bezpečné spracovanie GET parametra
$model = match($_GET['model'] ?? 'difficulty') {
    'difficulty'           => new DifficultyModel($pdo),
    'cooking_method'       => new CookingMethodModel($pdo),
    'recipe_category'      => new RecipeCategoryModel($pdo),
    'ingredient_category'  => new IngredientCategoryModel($pdo),
    default                => new DifficultyModel($pdo),
};

// Rovnaký kód funguje pre VŠETKY modely — polymorfizmus v praxi
$zaznam  = $model->getAll();
$pocet   = $model->getCount();
$popis   = $model->describe();
$suvislo = $model->countRelated();
💡
Čo sme práve urobili: Jedna stránka ciselniky.php obsluhuje všetky číselníky — recepty aj ingrediencie. Pridanie nového číselníka znamená len pridať jeden riadok do match() — stránka sa nemusí meniť.
📝 Úloha na T08

Čo treba spraviť do T08

Na hodine sme spolu urobili refaktoring a ModelInterface. Tvojou úlohou je to isté urobiť vo svojej aplikácii.

  • Doplň countRelated() do všetkých svojich číselníkových modelov — každý musí implementovať abstraktnú metódu zo svojho BaseModel.
  • Vytvor ModelInterface.php podľa ukážky z hodiny.
  • Doplň implements ModelInterface do BaseModel a do všetkých hlavných entít.
  • Refaktoruj hlavné entityRecipeModel, IngredientModel a ďalšie podľa tvojej aplikácie — premeň ich na samostatné triedy bez dedenia z BaseModel.
  • Uprav ciselniky.php — pridaj do match() aj číselníky ingrediencií, skontroluj že stránka funguje pre všetky modely.
💡
Daj AI svoj BaseModel a RecipeModel z T06 ako vzor — požiadaj ho aby podľa toho refaktoroval tvoje ostatné modely. Každý riadok musíš vedieť vysvetliť.

📤 Čo odovzdať do Teams

  • interfaces/ModelInterface.php
  • BaseModel.php — s countRelated() a implements ModelInterface
  • Všetky číselníkové modely — doplnená countRelated()
  • Hlavné entity — samostatné triedy s implements ModelInterface
  • ciselniky.php — funkčná pre všetky číselníky

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

T08

Návrhové vzory (Design Patterns)

Čo sa naučíme

  • Prečo návrhové vzory — opakovateľné riešenia bežných problémov.
  • Singleton — jedno PDO pripojenie pre celú aplikáciu.
  • Repositorysearch(array $filters) v RecipeModel.
  • Factory — továrňová metóda na vytváranie modelov.
  • Prehľad ďalších vzorov — Observer, Decorator (informačne).
💡 1. Prečo návrhové vzory

Pomenované riešenia bežných problémov

Návrhové vzory nie sú knižnice ani hotový kód — sú to osvedčené riešenia problémov ktoré sa opakujú v každom projekte. Keď ich poznáš, vieš ich rozpoznať, pomenovať a aplikovať.

💡
Dobrá správa: Niektoré vzory už používaš — len si nevedel/a ako sa volajú. RecipeModel ktorý máš z T07 je Repository pattern. match() v ciselniky.php je základ Factory pattern. Dnes im len dáme meno a vylepšíme ich.
🔒 2. Singleton — jedno PDO pripojenie

Problém ktorý Singleton rieši

Teraz máme connect.php ktorý vytvorí PDO objekt — a každý súbor ho musí načítať cez require. Ak by sme omylom zavolali connect.php viackrát, vytvorilo by sa viac pripojení k databáze. Singleton zaručí že pripojenie vznikne len raz — bez ohľadu na to koľkokrát ho zavoláme.

všeobecná ukážka — Singleton
class Singleton
{
    // Statická vlastnosť — zdieľaná medzi všetkými volaniami
    private static ?Singleton $instancia = null;

    // Private konštruktor — nikto nemôže urobiť new Singleton()
    private function __construct() {}

    // Jediný spôsob ako získať inštanciu
    public static function getInstance(): static
    {
        if (static::$instancia === null) {
            static::$instancia = new static();
        }
        return static::$instancia;
    }
}

$a = Singleton::getInstance();
$b = Singleton::getInstance();
// $a === $b  → true — oba ukazujú na rovnaký objekt
📄 Database.php — Singleton pre PDO
<?php
class Database
{
    private static ?PDO $instancia = null;

    // Private konštruktor — zabraňuje new Database()
    private function __construct() {}

    public static function getInstance(): PDO
    {
        if (self::$instancia === null) {
            self::$instancia = new PDO(
                'mysql:host=db;dbname=vegan;charset=utf8mb4',
                'root',
                'root',
                [
                    PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
                    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
                    PDO::ATTR_EMULATE_PREPARES   => false,
                ]
            );
        }
        return self::$instancia;
    }
}

Ako ho použijeme namiesto connect.php

pred — connect.php
// Každý súbor musí načítať connect.php
require 'connect.php';
$model = new RecipeModel($pdo);
po — Database Singleton
// Kedykoľvek potrebujeme PDO — zavoláme getInstance()
require 'Database.php';
$model = new RecipeModel(Database::getInstance());

// Druhý model — PDO sa NEVYTVORÍ znova, použije sa existujúce
$ciselnik = new DifficultyModel(Database::getInstance());
💡
static vs self: self::$instancia vždy odkazuje na triedu kde je kód napísaný.
static::$instancia odkazuje na triedu ktorá metódu volá — umožňuje dedenie Singletonu. Pre Database použijeme self — nechceme aby niekto dedil databázové pripojenie.
🔍 3. Repository pattern — search(array $filters)

RecipeModel je Repository — len sme to tak nenazývali

Repository pattern oddeľuje logiku prístupu k databáze od zvyšku aplikácie. Namiesto toho aby sme písali SQL priamo v stránke — máme model ktorý sa o všetko stará. To je presne čo robí RecipeModel. Dnes ho rozšírime o metódu search() — filtrovanie receptov podľa parametrov.

📄 RecipeModel.php — doplnená metóda search()
public function search(array $filters): array
{
    // Základ SQL — vždy rovnaký
    $sql    = "SELECT r.id, r.title, r.prep_time, r.cook_time, r.servings,
                      d.name AS difficulty
               FROM recipes r
               LEFT JOIN difficulties d ON d.id = r.difficulty_id
               WHERE 1=1";

    // Pole pre hodnoty — budeme ich pridávať dynamicky
    $params = [];

    // Pridáme podmienky len ak filter prišiel
    if (!empty($filters['difficulty_id'])) {
        $sql .= " AND r.difficulty_id = :difficulty_id";
        $params[':difficulty_id'] = $filters['difficulty_id'];
    }

    if (!empty($filters['cooking_method_id'])) {
        $sql .= " AND r.cooking_method_id = :cooking_method_id";
        $params[':cooking_method_id'] = $filters['cooking_method_id'];
    }

    if (!empty($filters['season_id'])) {
        $sql .= " AND r.season_id = :season_id";
        $params[':season_id'] = $filters['season_id'];
    }

    if (!empty($filters['hladaj'])) {
        $sql .= " AND r.title LIKE :hladaj";
        $params[':hladaj'] = '%' . $filters['hladaj'] . '%';
    }

    $sql .= " ORDER BY r.title";

    $stmt = $this->pdo->prepare($sql);
    $stmt->execute($params);
    return $stmt->fetchAll();
}
💡
WHERE 1=1 — prečo? Je to trik ktorý zjednodušuje dynamické SQL. 1=1 je vždy pravda — takže nezmení výsledok. Ale umožňuje nám pridávať ďalšie podmienky vždy cez AND bez toho aby sme riešili či je to prvá podmienka alebo nie.

Ako search() použijeme v recepty.php

📄 recepty.php — filtrovanie receptov
<?php
require 'Database.php';
require 'models/RecipeModel.php';
require 'models/DifficultyModel.php';

$pdo     = Database::getInstance();
$model   = new RecipeModel($pdo);
$difficulty = new DifficultyModel($pdo);

// Zozbierame filtre z GET parametrov
$filters = [
    'difficulty_id'      => $_GET['difficulty_id'] ?? '',
    'cooking_method_id'  => $_GET['cooking_method_id'] ?? '',
    'season_id'          => $_GET['season_id'] ?? '',
    'hladaj'             => $_GET['hladaj'] ?? '',
];

// Ak nie sú žiadne filtre — vráti všetky recepty
$recepty    = $model->search($filters);
$obtiaznosti = $difficulty->getAll();
?>

<!-- Filtrovací formulár -->
<form method="get">
    <select name="difficulty_id" class="form-select">
        <option value="">Všetky obtiažnosti</option>
        <?php foreach ($obtiaznosti as $o): ?>
        <option value="<?= $o['id'] ?>"
            <?= ($filters['difficulty_id'] == $o['id']) ? 'selected' : '' ?>>
            <?= htmlspecialchars($o['name']) ?>
        </option>
        <?php endforeach; ?>
    </select>
    <input type="text" name="hladaj" class="form-control"
           value="<?= htmlspecialchars($filters['hladaj']) ?>"
           placeholder="Hľadaj recept...">
    <button type="submit" class="btn btn-primary">Filtrovať</button>
</form>
🏭 4. Factory pattern — továrňová metóda

Factory namiesto match() v ciselniky.php

Factory pattern vytvára objekty bez toho aby volajúci kód vedel ako presne objekt vzniká. Namiesto match() priamo v ciselniky.php — presunieme logiku do samostatnej triedy. Pridanie nového modelu bude zmena na jednom mieste.

všeobecná ukážka — Factory
class TvarovaFactory
{
    public static function vytvor(string $typ): Tvar
    {
        return match($typ) {
            'kruh'    => new Kruh(),
            'stvorec' => new Stvorec(),
            default   => throw new InvalidArgumentException("Neznámy tvar: $typ"),
        };
    }
}

// Volajúci kód nemusí vedieť ako Kruh vzniká
$tvar = TvarovaFactory::vytvor('kruh');
📄 ModelFactory.php — factory pre číselníky
<?php
class ModelFactory
{
    public static function vytvor(string $typ, PDO $pdo): ModelInterface
    {
        return match($typ) {
            'difficulty'          => new DifficultyModel($pdo),
            'cooking_method'      => new CookingMethodModel($pdo),
            'recipe_category'     => new RecipeCategoryModel($pdo),
            'ingredient_category' => new IngredientCategoryModel($pdo),
            default               => throw new InvalidArgumentException(
                "Neznámy typ modelu: $typ"
            ),
        };
    }
}
ciselniky.php — pred a po Factory
// PRED — match() priamo v ciselniky.php
$model = match($_GET['model'] ?? 'difficulty') {
    'difficulty'          => new DifficultyModel($pdo),
    'cooking_method'      => new CookingMethodModel($pdo),
    'recipe_category'     => new RecipeCategoryModel($pdo),
    'ingredient_category' => new IngredientCategoryModel($pdo),
    default               => new DifficultyModel($pdo),
};

// PO — Factory trieda
$model = ModelFactory::vytvor($_GET['model'] ?? 'difficulty', $pdo);
// Kratšie, prehľadnejšie — logika je v ModelFactory
💡
Výhoda Factory: Keď pridáme nový číselník — zmeníme len ModelFactory.php. Všetky stránky ktoré Factory používajú dostanú nový model automaticky — bez toho aby sme menili každú stránku zvlášť.
📚 5. Ďalšie vzory — informačne

Vzory ktoré stretnete v praxi

Vzor Čo rieši Kde ho poznáte
Singleton Jediná inštancia — jedno DB pripojenie Dnes — Database::getInstance()
Repository Oddeľuje DB logiku od aplikácie Dnes — RecipeModel::search()
Factory Vytvára objekty podľa parametra Dnes — ModelFactory::vytvor()
Observer Objekt upozorní ostatných keď sa zmení JavaScript — addEventListener
Decorator Pridá funkcionalitu bez zmeny triedy Laravel — middleware, PHP atribúty
MVC Oddeľuje dáta, logiku a zobrazenie Laravel, Symfony — celá architektúra
💡
Observer v JavaScripte: addEventListener('click', function() {}) je Observer pattern — tlačidlo upozorní všetkých poslucháčov keď nastane udalosť. Vidíš? Vzory poznáš — len si nevedel/a ako sa volajú.
📝 Úloha na T09

Čo treba spraviť do T09

Na hodine sme spolu implementovali Singleton, search() a Factory. Tvojou úlohou je to isté zakomponovať do svojej aplikácie.

  • Vytvor Database.php — Singleton pre PDO. Nahraď require 'connect.php' za Database::getInstance() vo všetkých svojich súboroch.
  • Doplň search(array $filters) do svojho hlavného modelu — filtruj podľa číselníkov ktoré tvoja hlavná entita používa.
  • Vytvor ModelFactory.php — presun match() z ciselniky.php do Factory.
  • Uprav recepty.php (alebo ekvivalent) — pridaj filtrovací formulár ktorý používa search().
💡
Daj AI svoj RecipeModel a štruktúru tabuliek — požiadaj ho aby doplnil search() pre tvoje filtre. Skontroluj že SQL dotaz má WHERE 1=1 a dynamicky pridáva podmienky len pre neprázdne filtre.

📤 Čo odovzdať do Teams

  • Database.php — Singleton
  • Hlavný model — doplnená metóda search()
  • ModelFactory.php
  • Stránka výpisu — funkčný filtrovací formulár

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

T09

TEST 2 & M:N Logika & Zadanie projektu

TEST 8b

✏️ Písomný test 2

  • Trvanie: 15 minút, pero + papier, zákaz AI/PC/mobilov.
  • Látka: dedenie, abstrakcia, interface, polymorfizmus, návrhové vzory.
🔗 1. Many-to-Many vzťahy — prečo existujú

Keď jeden nestačí

Recept môže mať viac ingrediencií — a tá istá ingrediencia môže byť vo viac receptoch. Toto nevieme uložiť do jednej tabuľky. Potrebujeme prepojovaciu (pivot) tabuľku ktorá drží dvojice — recept + ingrediencia.

štruktúra M:N vzťahu
-- Hlavné tabuľky
recipes      (id, title, ...)
ingredients  (id, name, ...)

-- Pivot tabuľka — drží M:N väzbu
recipe_ingredients (
    recipe_id      → FK na recipes     ON DELETE CASCADE
    ingredient_id  → FK na ingredients ON DELETE RESTRICT
    amount         → množstvo
    unit_id        → jednotka
)

-- Jeden recept → viac riadkov v pivot tabuľke
-- Jedna ingrediencia → viac riadkov v pivot tabuľke
💡
ON DELETE CASCADE vs RESTRICT na pivot tabuľke:
recipe_id → CASCADE — ak zmažeme recept, zmažú sa aj jeho ingrediencie v pivot tabuľke — prepojenie bez receptu nemá zmysel.
ingredient_id → RESTRICT — nechceme zmazať ingredienciu ak ju používa recept — ochrana integrity dát.
⚙️ 2. OOP prístup — attach(), detach(), sync()

Tri metódy pre M:N operácie

Namiesto priameho písania SQL do stránky — M:N operácie zapuzdrujeme do troch metód v modeli hlavnej entity. Tento prístup používa aj Laravel (Eloquent).

📄 RecipeModel.php — M:N metódy
// Pridaj ingredienciu k receptu
public function attachIngredient(
    int $recipeId,
    int $ingredientId,
    float $amount,
    int $unitId,
    ?string $note = null
): void {
    $stmt = $this->pdo->prepare(
        "INSERT INTO recipe_ingredients
            (recipe_id, ingredient_id, amount, unit_id, note)
         VALUES
            (:recipe_id, :ingredient_id, :amount, :unit_id, :note)"
    );
    $stmt->execute([
        ':recipe_id'     => $recipeId,
        ':ingredient_id' => $ingredientId,
        ':amount'        => $amount,
        ':unit_id'       => $unitId,
        ':note'          => $note,
    ]);
}

// Odober ingredienciu z receptu
public function detachIngredient(int $recipeId, int $ingredientId): void
{
    $stmt = $this->pdo->prepare(
        "DELETE FROM recipe_ingredients
         WHERE recipe_id = :recipe_id
           AND ingredient_id = :ingredient_id"
    );
    $stmt->execute([
        ':recipe_id'     => $recipeId,
        ':ingredient_id' => $ingredientId,
    ]);
}

// Nastav presne tieto kategórie — ostatné odober
// Používa sa pri ukladaní formulára
public function syncCategories(int $recipeId, array $categoryIds): void
{
    // 1. Zmaž všetky existujúce prepojenia
    $stmt = $this->pdo->prepare(
        "DELETE FROM recipe_category_map WHERE recipe_id = :recipe_id"
    );
    $stmt->execute([':recipe_id' => $recipeId]);

    // 2. Vlož nové prepojenia
    $stmt = $this->pdo->prepare(
        "INSERT INTO recipe_category_map (recipe_id, category_id)
         VALUES (:recipe_id, :category_id)"
    );
    foreach ($categoryIds as $categoryId) {
        $stmt->execute([
            ':recipe_id'   => $recipeId,
            ':category_id' => (int) $categoryId,
        ]);
    }
}
ako sa metódy používajú v stránke
// Pridanie ingrediencie k receptu
$model->attachIngredient(
    recipeId: 1,
    ingredientId: 5,
    amount: 200,
    unitId: 1,
    note: 'nakrájané'
);

// Odobranie ingrediencie z receptu
$model->detachIngredient(recipeId: 1, ingredientId: 5);

// Sync kategórií pri ukladaní formulára
// — checkboxy vrátia pole id-čiek
$model->syncCategories(
    recipeId: 1,
    categoryIds: [2, 3]   // len tieto dve kategórie zostanú
);
💡
Rozdiel attach vs sync:
attach() — pridá jedno prepojenie. Ak už existuje, vyhodí chybu (duplicate key).
detach() — odoberie jedno konkrétne prepojenie.
sync() — zmaže všetky a vloží nové. Ideálne pri ukladaní checkboxov z formulára.
🎓 3. Zadanie semestrálneho projektu

Cieľ projektu

Vytvoriť funkčnú webovú aplikáciu s databázovým backendom postavenou na princípoch objektového programovania. Aplikácia musí byť spustiteľná cez Docker. Každý riadok kódu musíš vedieť vysvetliť — na obhajobe budeš kód vysvetľovať a meniť na požiadanie.

📁 Povinná štruktúra súborov

adresárová štruktúra projektu
projekt/
├── index.php                         → dashboard — súhrn a štatistiky
│
├── [hlavna-entita].php               → zoznam s filtrom
├── [hlavna-entita]-detail.php        → detail záznamu
├── [hlavna-entita]-pridat.php        → formulár — pridanie
├── [hlavna-entita]-upravit.php       → formulár — úprava
│
│   (ak máš dve hlavné entity, opakuj pre druhú)
├── [druha-entita].php
├── [druha-entita]-detail.php
├── [druha-entita]-pridat.php
├── [druha-entita]-upravit.php
│
├── admin/
│   ├── index.php                     → admin dashboard
│   ├── ciselniky.php                 → správa číselníkov
│   └── reset-db.php                  → obnovenie databázy
│
├── includes/
│   ├── nav.php                       → hlavná navigácia
│   └── admin-nav.php                 → admin navigácia
│
├── models/
│   ├── BaseModel.php                 → abstraktná trieda pre číselníky
│   ├── ModelInterface.php            → rozhranie pre všetky modely
│   ├── Database.php                  → Singleton pre PDO
│   ├── ModelFactory.php              → Factory pre číselníky
│   ├── [HlavnaEntita]Model.php       → model hlavnej entity
│   ├── [DruhaEntita]Model.php        → model druhej entity (ak existuje)
│   └── [Ciselnik]Model.php           → jeden súbor pre každý číselník
│
├── sql/
│   └── databaza.sql                  → SQL skript — štruktúra a vzorové dáta
│
└── assets/
    ├── css/style.css
    └── js/script.js

⚙️ Backend — OOP požiadavky

Povinné triedy:

  • Database.phpSingleton — jedno PDO pripojenie pre celú aplikáciu
  • ModelInterface.phpInterface — zmluva pre všetky modely: getAll(), getById(), getCount(), delete(), describe()
  • BaseModel.phpabstraktná trieda pre číselníky — povinné abstraktné metódy: describe(), countRelated() — zdedené metódy: getAll(), getById(), getCount(), insert(), update(), delete()
  • ModelFactory.phpFactory pattern — vytvorí správny číselníkový model podľa GET parametra

Číselníkové modely (dedia z BaseModel):

  • Aspoň 2 číselníky pre hlavnú entitu
  • Každý implementuje describe() a countRelated()

Modely hlavných entít (samostatné triedy, implements ModelInterface):

  • getAll(), getById(), getCount()
  • insert(array $data), update(int $id, array $data), delete(int $id)
  • search(array $filters) — dynamické filtrovanie
  • getLatest(int $limit) — posledné záznamy pre dashboard
  • attach(), detach(), sync() — M:N operácie

🗄️ Databáza — požiadavky

  • Aspoň 5 tabuliek vrátane aspoň jednej M:N prepojovacej
  • Všetky cudzie kľúče musia mať definované ON DELETE pravidlo — CASCADE, RESTRICT alebo SET NULL — s odôvodnením v komentári
  • SQL skript v sql/databaza.sql — spustiteľný cez admin/reset-db.php
  • Vzorové dáta — aspoň 5 záznamov v hlavnej tabuľke
  • Aplikácia beží cez Docker

🧭 Navigácia — požiadavky

  • Hlavná navigácia (includes/nav.php): Dashboard, hlavná entita, druhá entita (ak existuje), Admin
  • Admin navigácia (includes/admin-nav.php): Admin dashboard, Číselníky (odkaz pre každý číselník zvlášť cez GET parameter), Obnoviť databázu
  • Navigácia musí byť jedna include — nie kopírovaná do každého súboru

📄 Povinná funkcionalita stránok

index.php — Dashboard:

  • Celkový počet záznamov hlavných entít
  • Počty podľa číselníkov — countRelated()
  • Posledné 2 záznamy každej hlavnej entity — getLatest(2)

Zoznam hlavnej entity:

  • Filtrovací formulár — aspoň 2 filtre + textové vyhľadávanie
  • Výpis výsledkov — search(array $filters)
  • Tlačidlá [Detail] [Upraviť] [Zmazať] pri každom zázname
  • Mazanie cez POST s potvrdením confirm()

Detail:

  • Všetky stĺpce záznamu vrátane JOINov na číselníky
  • Zobrazenie M:N prepojení — ingrediencie, kategórie...

Pridanie / Úprava:

  • Formulár so všetkými poliami
  • Select/checkbox pre číselníky načítané z databázy
  • M:N výber — checkboxy pre prepojenia
  • Po uložení presmerovanie na zoznam alebo detail

admin/ciselniky.php:

  • Výber číselníka cez GET parameter — ModelFactory
  • Výpis záznamov s počtom prepojených (countRelated())
  • Inline formulár na pridanie nového záznamu
  • Úprava a mazanie — try/catch pre RESTRICT

admin/reset-db.php:

  • Varovanie a potvrdzovacie tlačidlo
  • Po potvrdení spustí sql/databaza.sql
  • Zobrazí výsledok — úspech alebo chybová správa

📊 Hodnotenie projektu

Finálny kód (20 bodov):

  • 5b — správna OOP architektúra (BaseModel, Interface, Singleton, Factory)
  • 5b — funkčný CRUD pre hlavnú entitu
  • 5b — M:N vzťah s attach/detach/sync
  • 5b — čistota kódu, štruktúra súborov, ON DELETE pravidlá

Záverečná obhajoba (40 bodov):

  • 15b — vysvetlenie ľubovoľnej časti vlastného kódu
  • 15b — operatívna zmena v kóde zadaná vyučujúcou (Live Coding)
  • 10b — správne používanie terminológie

💻 Ukážkové otázky a úlohy na obhajobe

Nasledujúce otázky a úlohy sú príkladmi čo ťa môže čakať na obhajobe. Nie sú to presné otázky — slúžia ako príprava.

Vysvetlenie kódu:

  • Vysvetli čo robí abstract v tvojom BaseModel
  • Prečo tvoj hlavný model nededí z BaseModel?
  • Čo je ModelInterface a prečo ho máš?
  • Vysvetli čo robí search() krok po kroku
  • Čo je WHERE 1=1 a prečo ho používame?
  • Vysvetli rozdiel private / protected / public
  • Čo robí Database::getInstance() a prečo je lepší ako connect.php?
  • Vysvetli čo robí sync() — krok po kroku
  • Vysvetli ON DELETE pravidlá vo svojej pivot tabuľke — prečo CASCADE na jednej strane a RESTRICT na druhej?

Live Coding — operatívne zmeny:

  • Pridaj do dashboardu nový ukazovateľ — počet záznamov podľa niektorého číselníka
  • Uprav zoznam — zmeň zoradenie výsledkov podľa iného stĺpca
  • Pridaj do admin navigácie odkaz na ďalší číselník
  • Uprav delete() aby pred zmazaním overila či záznam existuje
  • Zmeň sync() — pridaj kontrolu či je pole prepojení neprázdne
  • Pridaj do attach() try/catch — ošetri prípad keď prepojenie už existuje

Terminológia — vieš vysvetliť?

  • Dedenie, abstrakcia, zapuzdrenie, polymorfizmus
  • Interface vs abstraktná trieda — rozdiel a kedy použiť ktoré
  • Singleton, Factory, Repository pattern
  • M:N vzťah, pivot tabuľka, cudzí kľúč
  • ON DELETE CASCADE, RESTRICT, SET NULL
  • PDO, prepared statement, placeholder

📤 Úloha č. 8 — odovzdať do začiatku T10

  • Model hlavnej entity — súbor [NazovEntity]Model.php doplnený o metódy pre M:N operácie: attach(), detach(), sync()
  • Ak máš dve hlavné entity — odovzdaj oba modely

Deadline: Začiatok T10. Penalizácia za omeškanie –50 % za každý týždeň omeškania.

📅 Termíny

  • ZIP archív — odovzdať do Teams najneskôr do konca T12
  • Obhajoba — T10 až T12 alebo neskôr podľa vypísaných termínov

Penalizácia za omeškanie ZIP archívu: –50 % za každý týždeň omeškania.

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.

📤 Úloha č. 9 — odovzdanie celého projektu

  • Celý projekt zabalený v ZIP archívepriezvisko_tema.zip (napr. novak_knizknica.zip)
  • ZIP musí obsahovať všetky súbory projektu vrátane sql/databaza.sql
  • Projekt musí byť spustiteľný cez Docker bez ďalších úprav

Deadline: T13 — deň obhajoby. Penalizácia za omeškanie –50 % za každý týždeň omeškania.

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