Informačná hodina a inštalácia
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)
8 týždňov × 3b
Deadline: do začiatku ďalšej hodiny
Penalizácia za omeškanie –50 % týždenne
5. a 9. týždeň (2×8b)
15 minút, len pero a papier
5 tabuliek, Docker, OOP architektúra
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.
Základný balíček projektu
Pre prácu na projekte si stiahnite základný balíček:
📥 Stiahnuť mojprojekt.zipObsahuje: Dockerfile, .htaccess, httpd.conf, docker-compose.yml a priečinok www
⚡ Pre skúsených — Docker už používate
- Rozbaliť ZIP do pracovného priečinka.
- V termináli zadať:
Web beží na localhost:8080, DB na localhost:8081.
🛠 Pre začiatočníkov — Nová inštalácia
- Nainštalovať Docker Desktop (Windows/Mac) alebo Docker Engine (Linux).
- Nainštalovať VS Code + rozšírenie "Docker" a "PHP Extension Pack".
- Povoliť WSL2 (ak ste na Windows).
- 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:
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
Základné príkazy (Terminál)
Dátový návrh a MySQLi
📁 Materiály k tejto hodine
📄 Návrh dátového modelu (PDF) 🗄️ Schéma a dáta — aplikácia receptár (PDF)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ínusers— zatiaľ 1 záznam (admin) — pripravené na rozšírenieingredients— suroviny napojené na kategóriu, sezónu, base_unit a g_per_piecerecipes— hlavná entita — napojená na users, difficulties a cooking_methodsingredient_nutrition— M:N pivot — výživové hodnoty suroviny per 100grecipe_ingredients— M:N pivot — suroviny receptu + množstvo + jednotkarecipe_category_map— M:N pivot — recept môže byť v niekoľkých kategóriách naraz
id.
utf8mb4 zaručí správne zobrazenie diakritiky.
-- =============================================================================
-- 🌱 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';
-- =============================================================================
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.
mysqlije 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)
// 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)
// 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'];
}
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.
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
$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
$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.
$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
mysqli → query() → vráti mysqli_result
→ fetch_assoc() → vráti asociatívne pole s dátami.
Tretia trieda mysqli_stmt (prepared statements) príde v T03.
Pripojenie a práca s receptami cez inštanciu $conn
Pripojenie k databáze
<?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
<?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
<?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();
Č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.phpktorý:- načíta
connect.phpcezrequire_once - vykoná
DROP TABLE IF EXISTSaCREATE TABLEpre jednu vybranú tabuľku - vloží 5 záznamov naraz jedným
INSERT - vypíše všetky záznamy vrátane hlavičky tabuľky
- načíta
📤 Č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ázeoperacie.php— skript so všetkými operáciami
Deadline: pred začiatkom T03. Penalizácia za omeškanie –50 % za každý týždeň omeškania.
PDO a Vlastné modely
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
catchbloku, ž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
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
$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ží:
"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 = [
// 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
$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() — 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.
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.
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.
// ── 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
// 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
// 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
// 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()
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 {
// 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
$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
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());
}
$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."
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.
<?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.
Č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.
// 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
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
-- 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
<?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
<?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.";
}
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
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
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
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ý']
}
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'].
Č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
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ť
-- 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
);
-- 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
);
-- 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
);
NULL-able, inak databáza odmietne pravidlo nastaviť.
Rýchla referencia — kedy ktoré pravidlo
-- 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é
Čo treba spraviť do T04
-
Vytvor súbor
connect.phpcez pdo — prepíš svoje MySQLi pripojenie na PDO. Použi rovnaké hodnoty ako v pôvodnomconnect.php. -
Vyber si jeden číselník z tvojej databázy (tabuľka s
idaname) a napíš preň vlastný Model podľa vzoruDifficultyModel.php. Model musí mať metódygetAll(),getById()agetCount(). -
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?
- Ktoré tabuľky si vyžadujú
- 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 pripojenieMojModel.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)
Deadline: pred začiatkom T04. Penalizácia za omeškanie –50 % za každý týždeň omeškania.
Zapuzdrenie (Encapsulation)
Č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:
$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á.
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
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
}
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ť.
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;
}
}
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.
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 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.
Č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
$model = new DifficultyModel($pdo);
echo $model; // ❌ Error: Object of class DifficultyModel
// could not be converted to string
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ť.
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
__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.
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
}
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.
<?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;
}
}
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.
<?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 */ }
}
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é.
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é
// URL: ?model=users --
$table = $_GET['model'];
$pdo->query("SELECT * FROM " . $table);
// Útočník dostane tabuľku users!
✅ Bezpečné
// 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),
};
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.
<?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>
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.
Č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ť.
<?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 { ... }
// }
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.
Č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
idanamea slúžia ako zoznam hodnôt pre iné tabuľky. Pre každý číselník vytvor vlastný Model podľa vzoruDifficultyModelz hodiny — s metódamigetAll(),getById(),getCount(),insert(),update(),delete()acountRecipes()(alebocountItems()— podľa toho čo tvoj číselník počíta). -
Vytvor súbor
pouzi_Model.phpa 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.phppodľ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.phpzo 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
Deadline: pred začiatkom T05. Penalizácia za omeškanie –50 % za každý týždeň omeškania.
TEST 1 & Dedenie
TEST 8b✏️ Písomný test 1
- Trvanie: 15 minút, pero + papier, zákaz AI/PC/mobilov.
- Látka: PHP triedy, OOP syntax, PDO, MySQLi.
Dedenie (Inheritance)
- Kľúčové slovo
extends. - Rodičovská trieda vs. potomok — prepisovanie metód.
- Konštruktor rodiča cez
parent::__construct().
Abstraktné triedy
Abstract — povinná implementácia v potomkoch
- Kľúčové slovo
abstract— čo to znamená a prečo sa používa. - Abstraktné metódy — povinná implementácia v potomkoch.
- Rozdiel medzi abstraktnou triedou a bežnou triedou.
- Praktické použitie: spoločný základ pre modely (napr.
BaseModel). - Integrácia do projektu — refaktoring existujúcich tried.
Interface a Polymorfizmus
Rozhrania a polymorfné správanie
- Rozhranie
interface— zmluva bez implementácie. - Implementácia viacerých interfacov (
implements). - Rozdiel:
abstract classvs.interface. - Polymorfizmus — rôzne objekty, rovnaké volanie metódy.
- Type hinting a dependency injection v PHP.
Návrhové vzory (Design Patterns)
Singleton, Factory a ďalšie
- Singleton — jediná inštancia triedy (napr. DB spojenie).
- Factory — továrňová metóda na vytváranie objektov.
- Prečo návrhové vzory? Opakovateľné a udržateľné riešenia.
- Implementácia vzorov priamo v projekte.
- Prehľad ďalších vzorov: Repository, Observer (informačne).
TEST 2 & M:N Logika
TEST 8b✏️ Písomný test 2
- Trvanie: 15 minút, pero + papier, zákaz AI/PC/mobilov.
- Látka: dedenie, abstrakcia, interface, polymorfizmus, vzory.
Many-to-Many vzťahy
- Prepojovacia (pivot) tabuľka — návrh a implementácia.
- OOP prístup k M:N: metódy
attach(),detach(),sync().
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.
Odovzdávanie a prezentovanie zadaní
OBHAJOBAZá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.