Dnes z dátového modelu vzniknú skutočné tabuľky v databáze a prvý kód projektu. Dostaneš hotovú kostru: Composer, jedno pripojenie k databáze, rodičovskú triedu pre číselníky a vzor pre hlavnú entitu. Ty doplníš SQL pre svoje tabuľky, testovacie dáta a modely pre svoju tému. Na konci cvičenia máš vo svojom webovom priestore na sigma.tuke.sk stránku, ktorá vypíše záznamy z každej tvojej tabuľky. Pojmy sú v tabuľke pojmov.
Today the data model turns into real tables in the database and the first code of the project. You get a ready-made skeleton: Composer, one database connection, a parent class for lookup tables and an example for the main entity. You add the SQL for your tables, test data and the models for your topic. By the end of the lab you have a page in your web space on sigma.tuke.sk that lists the records of every table. The terms are in the table of terms.
TeóriaTheory
Prečo kostra vyzerá práve takto
Štruktúra, ktorú dostávaš, nie je vymyslená pre tento predmet. Je to tvar, ktorý má každý moderný PHP projekt: Laravel, Symfony, aj každý balík, ktorý si stiahneš z Packagist (verejný sklad PHP knižníc). Všade nájdeš to isté:
composer.jsonv koreni – popis projektu a pravidlo, kde sú triedy. Framework Laravel má v ňom"App\\": "app/", Symfony"App\\": "src/"; my máme to isté ako Symfony.src/(aleboapp/) – vlastný kód projektu, rozdelený do podpriečinkov podľa úlohy:Model/pre prácu s dátami, od 5. týždňaController/pre rozhodovanie aView/pre výstup. Symfony aj Laravel majú priečinkyControlleraModeldoslova.vendor/– cudzí kód, nikdy sa doň nesiaha rukou, vždy ho vyrobí Composer.- jeden vstupný súbor
index.php, ktorý načíta autoloader a spustí aplikáciu. Vo frameworkoch je topublic/index.php; u nás leží v koreni, lebo webový priestor na sigma.tuke.sk nemá samostatný priečinokpublic, a práve preto kód chránime cez.htaccess. - konfigurácia mimo kódu – heslá a nastavenia servera v súbore, ktorý nie je v repository (Laravel a Symfony používajú
.env, myconfig.php; princíp je rovnaký). - SQL v repository – schéma databázy je súčasť projektu, nie niečo, čo existuje len v databáze. Frameworky tomu hovoria migrácie (téma 9. týždňa);
schema.sqlje ich najjednoduchšia podoba.
Dôvod, prečo sa to oplatí naučiť: kto pozná tento tvar, otvorí cudzí projekt v Laravel alebo Symfony a vie, kde čo hľadať. A naopak, kto raz postavil kostru vlastnými rukami, rozumie, čo framework robí automaticky. Pri obhajobe sa budeme pýtať: prečo je vendor/ v .gitignore, prečo config.php nie je v repository, prečo trieda App\Model\X leží práve v src/Model/X.php.
Od dátového modelu k tabuľkám
Dátový model z minulého týždňa je návrh. Tabuľky vznikajú príkazom CREATE TABLE, pre každú tabuľku jeden. Na poradí záleží: tabuľka s cudzím kľúčom sa dá vytvoriť až vtedy, keď existuje tabuľka, na ktorú kľúč odkazuje. Preto sa najprv vytvárajú číselníky, potom hlavná entita a nakoniec prepojky. Pri mazaní je poradie opačné.
Každá tabuľka má ENGINE=InnoDB (jediný typ tabuliek v MariaDB, ktorý cudzie kľúče naozaj kontroluje) a kódovanie utf8mb4 (slovenčina aj emoji). Primárny kľúč je id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY: celé nezáporné číslo, ktoré databáza prideľuje sama. Cudzí kľúč sa zapisuje FOREIGN KEY (difficulty_id) REFERENCES difficulties(id) ON DELETE RESTRICT; pravidlo ON DELETE je to isté, ktoré si zdôvodňoval v README.
SQL pre tabuľky nepíšeš do Adminer a nezabudneš. Ukladá sa do repository ako súbor sql/schema.sql, a testovacie dáta ako sql/seed.sql. Keď si databázu pokazíš, spustíš oba súbory znova a máš čistý stav. Keď niekto iný otvorí tvoj projekt, z týchto dvoch súborov si vyrobí rovnakú databázu. Na začiatku schema.sql je preto DROP TABLE IF EXISTS pre každú tabuľku: súbor sa dá spustiť opakovane.
Viac: W3Schools: SQL FOREIGN KEY
JOIN: spojenie dvoch tabuliek v jednom dotaze
V tabuľke recipes je stĺpec difficulty_id s číslom. Používateľ chce vidieť „stredná”, nie „2”. Text je v tabuľke difficulties. JOIN povie databáze, aby ku každému riadku receptu pripojila riadok obtiažnosti, ktorý k nemu patrí:
SELECT r.title, r.prep_time, d.name AS difficulty_name
FROM recipes r
JOIN difficulties d ON d.id = r.difficulty_id
ORDER BY r.id DESC;
Ako to čítať:
FROM recipes r– začíname od receptov;rje skratka (alias), aby sme nemuseli písaťrecipes.pred každým stĺpcom.JOIN difficulties d ON d.id = r.difficulty_id– ku každému receptu pripoj tú obtiažnosť, ktorejidsa rovnádifficulty_idreceptu. Podmienka zaONje vždy „primárny kľúč jednej tabuľky = cudzí kľúč druhej”.d.name AS difficulty_name– stĺpecnamez obtiažnosti premenujeme, aby sa v PHP nepomýlil s inýmname; v$row['difficulty_name']je potom text.- Výsledok má toľko riadkov, koľko je receptov; každý má navyše stĺpce z obtiažnosti.
Je to ten istý JOIN, ktorý poznáš: JOIN (presne INNER JOIN) vráti len recepty, ktoré obtiažnosť majú – u nás vždy všetky, lebo difficulty_id je NOT NULL. LEFT JOIN by vrátil aj recepty bez obtiažnosti s NULL v difficulty_name; použijeme ho v 9. týždni pri štatistikách, kde chceme aj číselníkové hodnoty s nulovým počtom. Pri prepojkách (M:N) sa spájajú tri tabuľky za sebou: recipes → recipe_ingredients → ingredients; to príde v 5. týždni pri detaile receptu.
Viac: W3Schools: SQL JOIN
Composer a priečinok vendor
Composer je program pre príkazový riadok, ktorý sa stará o cudzí kód v PHP projekte. Je to pre PHP to, čo je npm pre JavaScript alebo pip pre Python. V Kubeflow je už nainštalovaný; na sigma.tuke.sk ho nespúšťame, tam sa len kopíruje výsledok.
Composer robí dve veci:
- Sťahuje knižnice. Keď v 6. týždni budeme potrebovať Twig, nenapíšeme ho sami ani nestiahneme ručne; do
composer.jsonpribudne riadok"twig/twig": "^3.0"a Composer ho stiahne aj so všetkým, čo Twig sám potrebuje. - Vyrába autoloader. Súbor
vendor/autoload.php, ktorý PHP naučí, kde hľadať triedy (vysvetlené nižšie pri PSR-4). Tento týždeň používame len túto druhú vec.
Všetko, čo Composer potrebuje vedieť, je v súbore composer.json v koreni projektu. V našom sú štyri časti:
| Časť | Čo znamená |
|---|---|
name, description, type |
meno a popis projektu; pre nás len informatívne |
require |
čo projekt potrebuje; zatiaľ len "php": ">=8.1" – od 6. týždňa sem pribudnú knižnice |
autoload → psr-4 |
pravidlo "App\\": "src/": triedy s adresou App\… sú v priečinku src/ |
config → platform |
"php": "8.1.2": Composer v Kubeflow beží na PHP 8.3, ale sigma.tuke.sk má 8.1; týmto riadkom vyberá len balíky, ktoré fungujú na 8.1 |
Príkazy, ktoré budeš používať:
| Príkaz | Čo urobí | Kedy |
|---|---|---|
composer install |
podľa composer.json vyrobí priečinok vendor/ a vendor/autoload.php |
po každom clone a vždy, keď sa zmení composer.json |
composer require twig/twig |
pridá knižnicu do composer.json a stiahne ju |
6. týždeň |
composer dump-autoload |
len znovu vyrobí autoloader | keď pridáš nový priečinok do src/ a PHP triedu nenájde |
Priečinok vendor/ je výsledok, nie zdroj. Do repository sa nedáva (je v .gitignore), lebo sa dá kedykoľvek vyrobiť znova z composer.json – rovnako ako sa do repository nedáva skompilovaný program, len jeho zdrojový kód. Dôsledok pre náš postup: po každom clone treba spustiť composer install, a keďže vendor/ nevzniká v editore, na sigma.tuke.sk ho dostaneš cez Upload Folder. Po composer install vznikne aj composer.lock – záznam presných verzií, ktoré sa nainštalovali; ten do repository patrí, aby mal každý rovnaké verzie.
Viac: getcomposer.org: Basic usage
Namespace: trieda má adresu
Každá trieda má meno, napríklad RecipeModel. V malom projekte to stačí. Vo väčšom projekte, kde sú aj cudzie knižnice, sa môžu dve triedy volať rovnako (každá knižnica môže mať svoju triedu User) a PHP by nevedelo, ktorú použiť. Preto má trieda okrem mena aj adresu, ktorej sa hovorí namespace. Píše sa na začiatok súboru:
namespace App\Model;
class RecipeModel
{
// ...
}
Plné meno tejto triedy je App\Model\RecipeModel: trieda RecipeModel v namespace App\Model. Čítaj to ako cestu: projekt App, časť Model, trieda RecipeModel. Dve triedy User v rôznych namespace si už neprekážajú, lebo majú rôzne plné mená.
Keď chceš triedu použiť v inom súbore, napíšeš hore use s plným menom, a ďalej už stačí krátke:
use App\Model\RecipeModel;
$model = new RecipeModel();
Bez use by si musel písať new \App\Model\RecipeModel() vždy celé. Oboje funguje; use je kratšie a používa sa všade.
PSR-4: adresa je zároveň cesta k súboru
Namespace rieši mená. Zostáva otázka, ako PHP nájde súbor, v ktorom je trieda. Doteraz si to riešil ručne: require_once 'RecipeModel.php' v každom súbore, ktorý triedu potreboval. Pri desiatich triedach je to desať riadkov, pri stovke neudržateľné.
PSR-4 je dohoda, že namespace zodpovedá priečinku a meno triedy názvu súboru. V composer.json je jedno pravidlo:
"autoload": {
"psr-4": { "App\\": "src/" }
}
Hovorí: všetko, čo začína App\, hľadaj v priečinku src/. Zvyšok adresy je cesta. Takže:
| Plné meno triedy | Súbor |
|---|---|
App\Database |
src/Database.php |
App\Model\RecipeModel |
src/Model/RecipeModel.php |
App\Model\CategoryModel |
src/Model/CategoryModel.php |
Composer z tohto pravidla vyrobí vendor/autoload.php. Ten súbor zaregistruje v PHP funkciu, ktorá sa spustí vždy, keď kód použije triedu, ktorú PHP ešte nepozná: podľa plného mena zloží cestu k súboru a súbor načíta. Tebe ostáva jediný require v celej aplikácii, v index.php:
require __DIR__ . '/vendor/autoload.php';
Od tej chvíle každá trieda „nájde sama seba”. Všetky require_once zmiznú. Dve pravidlá, ktoré musíš dodržať, inak autoloader súbor nenájde: jedna trieda = jeden súbor, a názov súboru sa presne zhoduje s menom triedy vrátane veľkých písmen (CategoryModel.php, nie categorymodel.php).
Viac: PHP-FIG: PSR-4
Kde sú prihlasovacie údaje a ako vzniká pripojenie
Prihlasovacie údaje k databáze – server, názov databázy, používateľ, heslo – sú na jednom jedinom mieste: v súbore config.php v koreni projektu. Vyzerá takto (s tvojimi údajmi):
<?php
return [
'host' => 'localhost', // databáza beží na tom istom serveri ako PHP
'dbname' => 'jana_novakova', // tvoja databáza – rovnaká ako login do Adminer
'user' => 'jana_novakova', // login do databázy
'pass' => 'heslo-z-cvicenia',
'debug' => true, // true = chyby vidieť v prehliadači (počas vývoja)
];
Súbor nerobí nič iné, než že vráti pole (return [...]). Kto údaje potrebuje, napíše $c = require 'config.php'; a má ich v $c['host'], $c['user'] atď. V celej aplikácii ich potrebuje jediná trieda: Database.
Prečo samostatný súbor: heslo nesmie byť v kóde, ktorý ide do repository. config.php je preto v .gitignore. V repository je len config.example.php – rovnaký súbor s prázdnym heslom, aby každý videl, aké položky treba vyplniť. Po clone si config.php vyrobíš kópiou (cp config.example.php config.php), doplníš údaje a uložíš; uložením v editore sa dostane aj na sigma.tuke.sk. Na GitHub nikdy nepôjde.
Pripojenie k databáze vzniká v triede Database v src/Database.php. Je to singleton: trieda, ktorá dovolí vytvoriť jediný objekt. Konštruktor je private, takže new Database() z iného miesta nejde; jediná cesta je Database::getInstance(). Tá pri prvom volaní prečíta config.php, zloží z neho DSN a vytvorí PDO; pri každom ďalšom volaní vráti to isté PDO, ktoré si drží v statickej vlastnosti $instance. Celá aplikácia tak zdieľa jedno pripojenie, nech sa getInstance() zavolá kdekoľvek a koľkokoľvek – každý model si ho vypýta vo svojom konštruktore.
DSN (data source name) je reťazec, ktorý PDO povie, kam sa pripojiť: mysql:host=localhost;dbname=jana_novakova;charset=utf8mb4. Skladá sa z hodnôt v config.php; používateľ a heslo idú do PDO ako samostatné parametre, nie do DSN. Na sigma.tuke.sk je databáza na tom istom serveri ako PHP, preto host je localhost.
Modely: opakovanie objektov a čo sa deje v kostre
Model je trieda, ktorá pracuje s jednou tabuľkou. Všetko ostatné v aplikácii (stránky, formuláre, neskôr API) sa k dátam dostáva len cez modely, nikdy priamym SQL. Pripomeňme si, z čoho sa trieda skladá, lebo v kostre je to všetko použité:
| Pojem | V kóde | Čo to je |
|---|---|---|
| trieda | class RecipeModel { } |
predpis: aké vlastnosti a metódy bude mať objekt |
| objekt | $m = new RecipeModel(); |
konkrétna vec vytvorená podľa predpisu |
| vlastnosť | protected string $table; |
premenná, ktorú objekt nesie v sebe |
| metóda | public function getAll(): array |
funkcia patriaca objektu; : array je typ, čo vracia |
$this |
$this->pdo |
odkaz na „tento objekt”, vnútri jeho metód |
| konštruktor | public function __construct() |
metóda, ktorá sa spustí pri new |
| viditeľnosť | public, protected, private |
kto smie k vlastnosti/metóde: ktokoľvek / trieda a potomkovia / len trieda |
| dedenie | class CategoryModel extends BaseModel |
potomok dostane všetko public a protected od rodiča |
| abstraktná trieda | abstract class BaseModel |
trieda len na dedenie, new BaseModel() nejde; abstraktná metóda nemá telo a potomok ju musí napísať |
| interface | interface ModelInterface / implements |
zoznam metód bez kódu; trieda, ktorá ho implementuje, musí všetky napísať |
| statická vlastnosť | private static ?PDO $instance |
patrí triede, nie objektu; všetky objekty ju zdieľajú |
| typ s otáznikom | ?array, ?PDO |
hodnota daného typu alebo null |
Interface ModelInterface hovorí, čo musí vedieť každý model: getAll(), getById(), getCount(), delete(), describe(). Nemá žiadny kód, len podpisy. Načo je dobrý: index.php prejde v jednom cykle všetky modely a na každom zavolá describe() a getAll() bez toho, aby vedel, ktorý je ktorý. PHP garantuje, že každá trieda s implements ModelInterface tie metódy má – inak by sa súbor ani nenačítal.
Číselníky dedia z BaseModel. Všetky číselníky sú rovnaké: tabuľka s id a name, vždy tie isté štyri dotazy. Preto sú tie dotazy napísané raz, v abstraktnej triede BaseModel, a každý číselník ich zdedí. Potomok má vlastného kódu päť riadkov: nastaví $table a napíše describe(). $table je protected, nie private, práve preto, aby ju potomok mohol nastaviť a rodičove metódy ju videli. BaseModel je abstract, lebo bez $table nemá zmysel; describe() je abstraktná, lebo každý číselník sa opíše inak.
Hlavná entita nededí. Jej getAll() potrebuje JOIN, aby namiesto difficulty_id = 2 vrátila aj text „stredná”. getById() tiež. Z BaseModel by jej ostalo takmer nič, a dediť kvôli dvom metódam, ktoré aj tak prepíšeš, je horšie ako napísať si ich sám. Preto hlavná entita implementuje ModelInterface priamo. Pravidlo: dedí sa vtedy, keď sa kód naozaj zdieľa, nie preto, že sa to dá.
Prepared statements (poznáš z OOP): SQL s pomenovaným placeholderom :id ide do databázy osobitne, hodnota osobitne v execute([':id' => $id]). Databáza hodnotu nikdy nepovažuje za SQL, takže SQL injection nie je možný. Dotazy bez vstupu od používateľa (SELECT * FROM … ORDER BY id) idú cez query(). Názov tabuľky v {$this->table} nie je vstup od používateľa – nastavuje ho kód triedy – preto môže byť v reťazci.
Čo nesmie byť vidieť z webu
Celý projekt leží vo webovom priestore, takže v zásade sa každý súbor dá otvoriť cez prehliadač. src/, vendor/ a sql/ nie sú stránky a config.php obsahuje heslo. Kostra to rieši v .htaccess: RedirectMatch 404 pre priečinky s kódom a <Files "config.php"> Require all denied </Files> pre konfiguráciu. Server odpovie 404 alebo 403 skôr, než sa k súboru dostane. Overené na sigma.tuke.sk; v úlohe 3.7 si to vyskúšaš sám.
Najčastejšie chyby a čo znamenajú
| Čo vidíš | Príčina | Riešenie |
|---|---|---|
Class "App\Model\CategoryModel" not found |
názov súboru sa nezhoduje s menom triedy, alebo chýba namespace App\Model; v súbore, alebo nebol spustený composer install |
skontroluj meno súboru vrátane veľkých písmen; composer dump-autoload |
Failed to open stream: vendor/autoload.php |
vendor/ nie je na sigma.tuke.sk |
composer install v Kubeflow, potom Upload Folder |
SQLSTATE[HY000] [1045] Access denied |
zlý používateľ alebo heslo v config.php |
porovnaj s prihlásením do Adminer |
SQLSTATE[HY000] [1049] Unknown database |
preklep v dbname |
názov databázy = tvoj login do Adminer |
SQLSTATE[42S02] Base table or view not found |
tabuľka neexistuje – schema.sql nebol spustený alebo má iný názov tabuľky než model |
spusti schema.sql v Adminer; porovnaj $table s CREATE TABLE |
SQLSTATE[23000] [1451] Cannot delete or update a parent row |
mažeš záznam, ktorý používa iná tabuľka s ON DELETE RESTRICT |
to je správne správanie; najprv zmaž závislé záznamy |
| prázdna biela stránka | chyba PHP a debug je false |
v config.php nastav 'debug' => true |
Why the skeleton looks the way it does
The structure you get was not invented for this course. It is the shape of every modern PHP project: Laravel, Symfony, and every package you download from Packagist (the public repository of PHP libraries). Everywhere you find the same things:
composer.jsonin the root – the project description and the rule where the classes are. Laravel has"App\\": "app/"in it, Symfony"App\\": "src/"; we have the same as Symfony.src/(orapp/) – the project’s own code, split into subfolders by role:Model/for data, from week 5Controller/for decisions andView/for output. Symfony and Laravel have folders literally calledControllerandModel.vendor/– third-party code, never touched by hand, always generated by Composer.- one entry file
index.php, which loads the autoloader and starts the application. In frameworks it ispublic/index.php; ours lies in the root because the web space on sigma.tuke.sk has no separatepublicfolder, which is exactly why we protect the code with.htaccess. - configuration outside the code – passwords and server settings in a file that is not in the repository (Laravel and Symfony use
.env, we useconfig.php; the principle is the same). - SQL in the repository – the database schema is part of the project, not something that exists only in the database. Frameworks call these migrations (week 9);
schema.sqlis their simplest form.
Why it is worth learning: whoever knows this shape opens somebody else’s Laravel or Symfony project and knows where to look. And the other way round: whoever has built the skeleton with their own hands understands what a framework does automatically. At the defence we will ask: why is vendor/ in .gitignore, why is config.php not in the repository, why does the class App\Model\X live exactly in src/Model/X.php.
From the data model to tables
Last week’s data model is a design. Tables are created with CREATE TABLE, one for each table. The order matters: a table with a foreign key can only be created once the table it points to exists. That is why lookup tables come first, then the main entity, and the pivot tables last. For dropping, the order is reversed.
Every table has ENGINE=InnoDB (the only table type in MariaDB that really checks foreign keys) and the utf8mb4 encoding (Slovak and emoji). The primary key is id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY: a non-negative integer the database assigns itself. A foreign key is written as FOREIGN KEY (difficulty_id) REFERENCES difficulties(id) ON DELETE RESTRICT; the ON DELETE rule is the one you justified in the README.
You do not type the SQL for the tables into Adminer and forget it. It is stored in the repository as the file sql/schema.sql, and the test data as sql/seed.sql. If you break your database, you run both files again and have a clean state. When somebody else opens your project, they build the same database from these two files. That is why schema.sql starts with DROP TABLE IF EXISTS for every table: the file can be run repeatedly.
More: W3Schools: SQL FOREIGN KEY
JOIN: two tables in one query
The table recipes has the column difficulty_id with a number. The user wants to see “medium”, not “2”. The text is in the table difficulties. JOIN tells the database to attach to every recipe row the difficulty row that belongs to it:
SELECT r.title, r.prep_time, d.name AS difficulty_name
FROM recipes r
JOIN difficulties d ON d.id = r.difficulty_id
ORDER BY r.id DESC;
How to read it:
FROM recipes r– we start from the recipes;ris an alias so that we do not writerecipes.before every column.JOIN difficulties d ON d.id = r.difficulty_id– to every recipe attach the difficulty whoseidequals the recipe’sdifficulty_id. The condition afterONis always “primary key of one table = foreign key of the other”.d.name AS difficulty_name– the columnnamefrom the difficulty is renamed so that it is not confused with anothernamein PHP;$row['difficulty_name']then holds the text.- The result has as many rows as there are recipes; each has the difficulty columns in addition.
It is the same JOIN you know: JOIN (precisely INNER JOIN) returns only recipes that have a difficulty – for us always all of them, because difficulty_id is NOT NULL. LEFT JOIN would also return recipes without a difficulty with NULL in difficulty_name; we use it in week 9 for statistics, where we also want lookup values with a count of zero. With pivot tables (M:N) three tables are joined in a row: recipes → recipe_ingredients → ingredients; that comes in week 5 with the recipe detail.
More: W3Schools: SQL JOIN
Composer and the vendor folder
Composer is a command-line program that manages third-party code in a PHP project. It is for PHP what npm is for JavaScript or pip for Python. It is already installed in Kubeflow; we never run it on sigma.tuke.sk, only the result is copied there.
Composer does two things:
- It downloads libraries. When we need Twig in week 6, we neither write it ourselves nor download it by hand; a line
"twig/twig": "^3.0"is added tocomposer.jsonand Composer downloads it together with everything Twig itself needs. - It generates the autoloader. The file
vendor/autoload.php, which teaches PHP where to find classes (explained below under PSR-4). This week we use only this second thing.
Everything Composer needs to know is in the file composer.json in the project root. Ours has four parts:
| Part | Meaning |
|---|---|
name, description, type |
project name and description; informational for us |
require |
what the project needs; for now only "php": ">=8.1" – from week 6 libraries are added here |
autoload → psr-4 |
the rule "App\\": "src/": classes with the address App\… are in the src/ folder |
config → platform |
"php": "8.1.2": Composer in Kubeflow runs on PHP 8.3, but sigma.tuke.sk has 8.1; with this line it picks only packages that work on 8.1 |
Commands you will use:
| Command | What it does | When |
|---|---|---|
composer install |
creates the vendor/ folder and vendor/autoload.php according to composer.json |
after every clone and whenever composer.json changes |
composer require twig/twig |
adds a library to composer.json and downloads it |
week 6 |
composer dump-autoload |
only regenerates the autoloader | when you add a new folder to src/ and PHP cannot find a class |
The vendor/ folder is a result, not a source. It is not put into the repository (it is in .gitignore), because it can be regenerated from composer.json at any time – just as a compiled program is not put into a repository, only its source code. The consequence for our workflow: after every clone you run composer install, and since vendor/ is not created in the editor, you get it to sigma.tuke.sk with Upload Folder. composer install also creates composer.lock – a record of the exact versions installed; that one does belong in the repository so that everybody has the same versions.
More: getcomposer.org: Basic usage
Namespace: a class has an address
Every class has a name, for example RecipeModel. In a small project that is enough. In a bigger project with third-party libraries, two classes can have the same name (every library may have its own User class) and PHP would not know which one to use. That is why a class has, besides its name, an address, called a namespace. It is written at the top of the file:
namespace App\Model;
class RecipeModel
{
// ...
}
The full name of this class is App\Model\RecipeModel: the class RecipeModel in the namespace App\Model. Read it like a path: project App, part Model, class RecipeModel. Two User classes in different namespaces no longer clash, because their full names differ.
When you want to use the class in another file, you write use with the full name at the top, and then the short name is enough:
use App\Model\RecipeModel;
$model = new RecipeModel();
Without use you would have to write new \App\Model\RecipeModel() in full every time. Both work; use is shorter and used everywhere.
PSR-4: the address is also the path to the file
Namespaces solve names. The remaining question is how PHP finds the file the class is in. Until now you did it by hand: require_once 'RecipeModel.php' in every file that needed the class. With ten classes that is ten lines; with a hundred it is unmaintainable.
PSR-4 is the convention that the namespace matches the folder and the class name matches the file name. In composer.json there is one rule:
"autoload": {
"psr-4": { "App\\": "src/" }
}
It says: everything starting with App\ is in the src/ folder. The rest of the address is the path. So:
| Full class name | File |
|---|---|
App\Database |
src/Database.php |
App\Model\RecipeModel |
src/Model/RecipeModel.php |
App\Model\CategoryModel |
src/Model/CategoryModel.php |
From this rule Composer generates vendor/autoload.php. That file registers a function in PHP which runs whenever the code uses a class PHP does not know yet: from the full name it builds the path to the file and loads it. You are left with a single require in the whole application, in index.php:
require __DIR__ . '/vendor/autoload.php';
From then on every class “finds itself”. All require_once lines disappear. Two rules you must keep, otherwise the autoloader will not find the file: one class = one file, and the file name matches the class name exactly, including capital letters (CategoryModel.php, not categorymodel.php).
More: PHP-FIG: PSR-4
Where the login details are and how the connection is made
The database login details – server, database name, user, password – are in one single place: the file config.php in the project root. It looks like this (with your details):
<?php
return [
'host' => 'localhost', // the database runs on the same server as PHP
'dbname' => 'jana_novakova', // your database – the same as your Adminer login
'user' => 'jana_novakova', // database login
'pass' => 'password-from-lab',
'debug' => true, // true = errors shown in the browser (during development)
];
The file does nothing except return an array (return [...]). Whoever needs the details writes $c = require 'config.php'; and has them in $c['host'], $c['user'] and so on. In the whole application only one class needs them: Database.
Why a separate file: the password must not be in code that goes into the repository. That is why config.php is in .gitignore. The repository only has config.example.php – the same file with an empty password, so that everybody sees which items to fill in. After the clone you create config.php as a copy (cp config.example.php config.php), fill in the details and save; saving in the editor also gets it to sigma.tuke.sk. It never goes to GitHub.
The database connection is made in the class Database in src/Database.php. It is a singleton: a class that allows only one object to be created. The constructor is private, so new Database() from elsewhere is not possible; the only way is Database::getInstance(). On the first call it reads config.php, builds the DSN from it and creates the PDO; on every later call it returns the same PDO, which it keeps in the static property $instance. The whole application thus shares one connection, no matter where and how many times getInstance() is called – every model asks for it in its constructor.
A DSN (data source name) is the string that tells PDO where to connect: mysql:host=localhost;dbname=jana_novakova;charset=utf8mb4. It is built from the values in config.php; the user and the password go to PDO as separate parameters, not into the DSN. On sigma.tuke.sk the database is on the same server as PHP, hence host is localhost.
Models: a recap of objects and what happens in the skeleton
A model is a class that works with one table. Everything else in the application (pages, forms, later the API) reaches the data only through models, never with direct SQL. Let us recall what a class consists of, because all of it is used in the skeleton:
| Term | In code | What it is |
|---|---|---|
| class | class RecipeModel { } |
a blueprint: which properties and methods an object will have |
| object | $m = new RecipeModel(); |
a concrete thing created from the blueprint |
| property | protected string $table; |
a variable the object carries inside |
| method | public function getAll(): array |
a function belonging to the object; : array is the type it returns |
$this |
$this->pdo |
a reference to “this object”, inside its methods |
| constructor | public function __construct() |
the method that runs on new |
| visibility | public, protected, private |
who may access a property/method: anybody / the class and its children / the class only |
| inheritance | class CategoryModel extends BaseModel |
the child gets everything public and protected from the parent |
| abstract class | abstract class BaseModel |
a class meant only to be inherited, new BaseModel() is not possible; an abstract method has no body and the child must write it |
| interface | interface ModelInterface / implements |
a list of methods without code; a class implementing it must write them all |
| static property | private static ?PDO $instance |
belongs to the class, not to an object; all objects share it |
| nullable type | ?array, ?PDO |
a value of that type or null |
The interface ModelInterface says what every model must be able to do: getAll(), getById(), getCount(), delete(), describe(). It has no code, only signatures. What it is good for: index.php goes through all models in one loop and calls describe() and getAll() on each, without knowing which is which. PHP guarantees that every class with implements ModelInterface has those methods – otherwise the file would not even load.
Lookup models inherit from BaseModel. All lookup tables are alike: a table with id and name, always the same four queries. That is why those queries are written once, in the abstract class BaseModel, and every lookup model inherits them. The child has five lines of its own code: it sets $table and writes describe(). $table is protected, not private, precisely so that the child can set it and the parent’s methods can see it. BaseModel is abstract because without $table it makes no sense; describe() is abstract because every lookup table describes itself differently.
The main entity does not inherit. Its getAll() needs a JOIN so that instead of difficulty_id = 2 it also returns the text “medium”. So does getById(). Almost nothing of BaseModel would remain, and inheriting for the sake of two methods you override anyway is worse than writing them yourself. That is why the main entity implements ModelInterface directly. The rule: inherit when code is really shared, not just because it is possible.
Prepared statements (you know them from OOP): the SQL with a named placeholder :id goes to the database separately, the value separately in execute([':id' => $id]). The database never treats the value as SQL, so SQL injection is not possible. Queries without user input (SELECT * FROM … ORDER BY id) go through query(). The table name in {$this->table} is not user input – the class code sets it – so it may be in the string.
What must not be visible from the web
The whole project lies in your web space, so in principle every file can be opened in the browser. src/, vendor/ and sql/ are not pages, and config.php contains the password. The skeleton handles this in .htaccess: RedirectMatch 404 for the code folders and <Files "config.php"> Require all denied </Files> for the configuration. The server answers 404 or 403 before it reaches the file. Verified on sigma.tuke.sk; in task 3.7 you check it yourself.
The most common errors and what they mean
| What you see | Cause | Fix |
|---|---|---|
Class "App\Model\CategoryModel" not found |
the file name does not match the class name, or namespace App\Model; is missing in the file, or composer install was not run |
check the file name including capital letters; composer dump-autoload |
Failed to open stream: vendor/autoload.php |
vendor/ is not on sigma.tuke.sk |
composer install in Kubeflow, then Upload Folder |
SQLSTATE[HY000] [1045] Access denied |
wrong user or password in config.php |
compare with your Adminer login |
SQLSTATE[HY000] [1049] Unknown database |
typo in dbname |
the database name = your Adminer login |
SQLSTATE[42S02] Base table or view not found |
the table does not exist – schema.sql was not run, or the model has a different table name |
run schema.sql in Adminer; compare $table with CREATE TABLE |
SQLSTATE[23000] [1451] Cannot delete or update a parent row |
you are deleting a record another table uses with ON DELETE RESTRICT |
that is correct behaviour; delete the dependent records first |
| a blank white page | a PHP error and debug is false |
set 'debug' => true in config.php |
UkážkyExamples
Ukážka 1: štruktúra kostry
Kostra je na stiahnutie ako kostra.zip; rozbalená do priečinka projektu vyzerá takto:
cv02/
├── .htaccess src/, vendor/, sql/ a config.php nie sú z webu prístupné
├── .gitignore config.php, vendor/, .vscode/ nejdú do repository
├── composer.json autoload PSR-4: App\ -> src/
├── config.example.php vzor konfigurácie bez hesla (v repository)
├── config.php tvoja konfigurácia s heslom (len v Kubeflow a na sigma.tuke.sk)
├── index.php kontrolná stránka
├── sql/
│ ├── schema.sql CREATE TABLE pre všetky tabuľky
│ └── seed.sql testovacie dáta
├── src/
│ ├── Database.php App\Database – jedno PDO pripojenie
│ └── Model/
│ ├── ModelInterface.php
│ ├── BaseModel.php rodič číselníkov
│ ├── LookupModel.php vzor číselníka (zmažeš, keď máš svoje)
│ └── MainModel.php vzor hlavnej entity (zmažeš, keď máš svoje)
└── vendor/ vyrobí composer install (nie je v repository)
└── autoload.php
Ukážka 2: schema.sql vzorového projektu
Tabuľky receptov z minulého týždňa, v poradí, v akom sa dajú vytvoriť. Všimni si, že DROP TABLE ide v opačnom poradí ako CREATE TABLE.
-- Mazanie v opačnom poradí: najprv prepojky, potom hlavná entita, nakoniec číselníky.
DROP TABLE IF EXISTS recipe_ingredients;
DROP TABLE IF EXISTS recipe_categories;
DROP TABLE IF EXISTS recipes;
DROP TABLE IF EXISTS units;
DROP TABLE IF EXISTS ingredients;
DROP TABLE IF EXISTS categories;
DROP TABLE IF EXISTS difficulties;
-- Číselník: obtiažnosť receptu (ľahký, stredný, náročný).
CREATE TABLE difficulties (
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(50) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Číselník: kategória jedla (polievka, hlavné jedlo, dezert).
CREATE TABLE categories (
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(50) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Číselník: surovina.
CREATE TABLE ingredients (
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(80) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Číselník: merná jednotka (g, ml, ks).
CREATE TABLE units (
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(30) NOT NULL,
abbreviation VARCHAR(10) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Hlavná entita: recept.
CREATE TABLE recipes (
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(120) NOT NULL,
servings TINYINT UNSIGNED NOT NULL DEFAULT 4,
prep_time SMALLINT UNSIGNED NOT NULL, -- minúty
instructions TEXT NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
difficulty_id INT UNSIGNED NOT NULL,
-- 1:N na číselník. RESTRICT: obtiažnosť sa nedá zmazať, kým ju používa recept.
FOREIGN KEY (difficulty_id) REFERENCES difficulties(id) ON DELETE RESTRICT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Prepojka bez atribútov: recept × kategória.
CREATE TABLE recipe_categories (
recipe_id INT UNSIGNED NOT NULL,
category_id INT UNSIGNED NOT NULL,
PRIMARY KEY (recipe_id, category_id),
-- CASCADE: so zmazaným receptom zmiznú aj jeho prepojenia.
FOREIGN KEY (recipe_id) REFERENCES recipes(id) ON DELETE CASCADE,
-- RESTRICT: kategória sa nedá zmazať, kým ju používa recept.
FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE RESTRICT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Prepojka s atribútmi: recept × surovina, s množstvom a jednotkou.
CREATE TABLE recipe_ingredients (
recipe_id INT UNSIGNED NOT NULL,
ingredient_id INT UNSIGNED NOT NULL,
amount DECIMAL(8,2) NOT NULL,
note VARCHAR(100) NULL,
unit_id INT UNSIGNED NOT NULL,
PRIMARY KEY (recipe_id, ingredient_id),
FOREIGN KEY (recipe_id) REFERENCES recipes(id) ON DELETE CASCADE,
FOREIGN KEY (ingredient_id) REFERENCES ingredients(id) ON DELETE RESTRICT,
FOREIGN KEY (unit_id) REFERENCES units(id) ON DELETE RESTRICT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
Ukážka 3: composer.json
{
"name": "tuke/it-projekt",
"description": "Semestrálny projekt – Informačné technológie",
"type": "project",
"require": {
"php": ">=8.1"
},
"autoload": {
"psr-4": {
"App\\": "src/"
}
},
"config": {
"platform": {
"php": "8.1.2"
}
}
}
Jediné, čo sa v ňom počas semestra zmení, je require: keď v 6. týždni pridáme Twig, pribudne riadok a composer install ho stiahne.
Ukážka 4: Database.php – jedno pripojenie
<?php
declare(strict_types=1);
namespace App;
use PDO;
final class Database
{
private static ?PDO $instance = null;
// Súkromný konštruktor: new Database() nie je možné.
private function __construct()
{
}
public static function getInstance(): PDO
{
if (self::$instance === null) {
$c = require __DIR__ . '/../config.php';
$dsn = "mysql:host={$c['host']};dbname={$c['dbname']};charset=utf8mb4";
self::$instance = new PDO($dsn, $c['user'], $c['pass'], [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]);
}
return self::$instance;
}
}
Čo robí ktorý riadok v Database.php:
declare(strict_types=1);– PHP nebude ticho prevádzať typy (reťazec na číslo a pod.); chyba typu sa ozve hneď. Píšeme do každého súboru.namespace App;– adresa triedy; plné meno jeApp\Database, súbor jesrc/Database.php.use PDO;– PDO je zabudovaná trieda PHP bez namespace; keďže my sme v namespaceApp, musíme povedať, že myslíme „tú globálnu”.final class– z triedy sa nedá dediť; pre singleton je to úmysel.private static ?PDO $instance = null;– jediné miesto, kde pripojenie žije.static= patrí triede, nie objektu;?PDO= PDO alebonull; na začiatkunull, lebo pripojenie ešte neexistuje.private function __construct() {}– súkromný konštruktor:new Database()zvonku skončí chybou. Toto robí zo triedy singleton.if (self::$instance === null)–self::je prístup k statickej vlastnosti vlastnej triedy. Pripojenie sa vytvorí len prvýkrát.require __DIR__ . '/../config.php'–__DIR__je priečinok tohto súboru (src/),..ide o úroveň vyššie, kde jeconfig.php.requiretu vráti pole zreturn.new PDO($dsn, $c['user'], $c['pass'], [...])– samotné pripojenie. Tretí a štvrtý parameter sú používateľ a heslo zconfig.php.ATTR_ERRMODE => ERRMODE_EXCEPTION– chyba v SQL vyhodí výnimku (PDOException), ktorú vidíš v prehliadači, keď jedebugzapnutý; bez toho by chyby zostali ticho.ATTR_DEFAULT_FETCH_MODE => FETCH_ASSOC– riadky z databázy prídu ako asociatívne polia ($row['title']), nie aj s číselnými kľúčmi.ATTR_EMULATE_PREPARES => false– prepared statements rieši databáza, nie PHP; bezpečnejšie a typy sedia.return self::$instance;– vždy to isté PDO.
Ukážka 5: ModelInterface a BaseModel
<?php
declare(strict_types=1);
namespace App\Model;
interface ModelInterface
{
public function getAll(): array;
public function getById(int $id): ?array;
public function getCount(): int;
public function delete(int $id): bool;
public function describe(): string;
}
<?php
declare(strict_types=1);
namespace App\Model;
use App\Database;
use PDO;
abstract class BaseModel implements ModelInterface
{
protected PDO $pdo;
protected string $table; // nastaví každý potomok
public function __construct()
{
$this->pdo = Database::getInstance();
}
public function getAll(): array
{
return $this->pdo->query("SELECT * FROM {$this->table} ORDER BY id")->fetchAll();
}
public function getById(int $id): ?array
{
$stmt = $this->pdo->prepare("SELECT * FROM {$this->table} WHERE id = :id");
$stmt->execute([':id' => $id]);
$row = $stmt->fetch();
return $row === false ? null : $row;
}
public function getCount(): int
{
return (int) $this->pdo->query("SELECT COUNT(*) FROM {$this->table}")->fetchColumn();
}
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;
}
abstract public function describe(): string;
}
Čo robí ktorý riadok v BaseModel.php:
abstract class BaseModel implements ModelInterface– rodič číselníkov; zaväzuje sa k rozhraniu, ale sám sa nedá vytvoriť.protected PDO $pdo;– pripojenie, ktoré budú používať všetky metódy;protected, aby ho videli aj potomkovia.protected string $table;– názov tabuľky, zámerne bez hodnoty: doplní ho každý potomok.$this->pdo = Database::getInstance();– v konštruktore si model vypýta spoločné pripojenie. Potomok konštruktor nepíše, zdedí tento.getAll():query()spustí dotaz bez parametrov,fetchAll()vráti všetky riadky ako pole polí.{$this->table}sa doplní do reťazca.getById():prepare()pripraví dotaz s:id,execute([':id' => $id])dosadí hodnotu,fetch()vráti jeden riadok alebofalse, keď nič nenašiel. Pretoreturn $row === false ? null : $row;– metóda sľubuje?array, teda pole alebonull, niefalse.getCount():fetchColumn()vráti prvý stĺpec prvého riadka – priCOUNT(*)priamo číslo;(int)ho prevedie z reťazca na celé číslo, lebo metóda sľubujeint.delete(): poexecute()povierowCount(), koľko riadkov sa zmazalo;> 0dátrue/false. Ak záznam používa iná tabuľka sON DELETE RESTRICT, databáza zmazanie odmietne a vyhodí výnimku – to je správne správanie, nie chyba kódu.abstract public function describe(): string;– bez tela; každý potomok ju musí napísať.
Ukážka 6: modely vzorového projektu
Číselník má päť riadkov vlastného kódu. Hlavná entita nededí, lebo jej dotazy sú iné: JOIN doplní názov obtiažnosti.
<?php
declare(strict_types=1);
namespace App\Model;
class DifficultyModel extends BaseModel
{
protected string $table = 'difficulties';
public function describe(): string
{
return 'Číselník obtiažností';
}
}
<?php
declare(strict_types=1);
namespace App\Model;
use App\Database;
use PDO;
class RecipeModel implements ModelInterface
{
private PDO $pdo;
public function __construct()
{
$this->pdo = Database::getInstance();
}
// Recepty aj s názvom obtiažnosti, najnovšie prvé.
public function getAll(): array
{
$sql = "SELECT r.*, d.name AS difficulty_name
FROM recipes r
JOIN difficulties d ON d.id = r.difficulty_id
ORDER BY r.id DESC";
return $this->pdo->query($sql)->fetchAll();
}
public function getById(int $id): ?array
{
$sql = "SELECT r.*, d.name AS difficulty_name
FROM recipes r
JOIN difficulties d ON d.id = r.difficulty_id
WHERE r.id = :id";
$stmt = $this->pdo->prepare($sql);
$stmt->execute([':id' => $id]);
$row = $stmt->fetch();
return $row === false ? null : $row;
}
public function getCount(): int
{
return (int) $this->pdo->query("SELECT COUNT(*) FROM recipes")->fetchColumn();
}
public function delete(int $id): bool
{
$stmt = $this->pdo->prepare("DELETE FROM recipes WHERE id = :id");
$stmt->execute([':id' => $id]);
return $stmt->rowCount() > 0;
}
public function describe(): string
{
return 'Recepty';
}
}
Čo robí ktorý riadok v modeloch:
DifficultyModel extends BaseModel– zdedí konštruktor,$pdoa štyri metódy.protected string $table = 'difficulties';prepíše rodičovu prázdnu vlastnosť hodnotou.describe()je jediná metóda, ktorú musí napísať, lebo v rodičovi je abstraktná. Celá trieda má päť riadkov vlastného kódu – to je zmysel dedenia.RecipeModel implements ModelInterface– nededí, preto má vlastnéprivate PDO $pdo;(tu môže byťprivate, nikto z nej nededí) a vlastný konštruktor, ktorý si vypýta pripojenie rovnako akoBaseModel.SELECT r.*, d.name AS difficulty_name FROM recipes r JOIN difficulties d ON d.id = r.difficulty_id–radsú skratky (aliasy) tabuliek.r.*= všetky stĺpce receptu,d.name AS difficulty_name= k tomu názov obtiažnosti pod zrozumiteľným menom.JOIN … ONspojí riadok receptu s riadkom obtiažnosti, ktorý má rovnakéidakodifficulty_id. Vo výsledku je$row['difficulty_name']= „stredná” namiesto čísla 2.ORDER BY r.id DESC– najnovšie recepty prvé; číselníky majúORDER BY id, lebo poradie vloženia je aj poradie zobrazenia.getById()s tým istýmJOINaWHERE r.id = :id– pri aliasoch treba povedať, ktorej tabuľkyidmyslíme.getCount()adelete()sú rovnaké ako vBaseModel, len s pevným názvom tabuľky.
V index.php sa potom modely použijú rovnako, bez ohľadu na to, či dedia alebo nie:
$models = [
new \App\Model\DifficultyModel(),
new \App\Model\CategoryModel(),
new \App\Model\RecipeModel(),
];
Čo robí index.php z kostry (kontrolná stránka):
require __DIR__ . '/vendor/autoload.php';– jedinýrequirev aplikácii; od tohto riadka PHP nájde každú trieduApp\…samo.$config = require __DIR__ . '/config.php';aif ($config['debug']) { ini_set('display_errors', '1'); error_reporting(E_ALL); }– sigma.tuke.sk má zobrazovanie chýb vypnuté; počas vývoja ich chceme vidieť, preto ich zapneme, keď je v konfiguráciidebugnatrue.use App\Database;a$pdo = Database::getInstance();– prvé a jediné vytvorenie pripojenia; modely dostanú to isté.$pdo->query('SHOW TABLES')->fetchAll(PDO::FETCH_COLUMN)– zoznam tabuliek v databáze ako jednoduché pole názvov; funguje ešte pred tým, než máš akýkoľvek model.$models = [ new \App\Model\CategoryModel(), … ];– zoznam tvojich modelov; doplníš jeden riadok na triedu. Plné meno s\na začiatku preto, žeindex.phpnie je v žiadnom namespace ausesme použili len preDatabase.foreach ($models as $model)– pre každý modeldescribe(),getCount()a prvých päť riadkov zgetAll()cezarray_slice. Hlavičku tabuľky vyrobíarray_keys($rows[0])– názvy stĺpcov z prvého riadka.htmlspecialchars(...)pri každom výpise – hodnoty z databázy sa nevypisujú priamo, aby prípadné<v dátach nerozbilo stránku; ochrana pred XSS, podrobnejšie v 6. týždni.
Ukážka 7: prompt na tabuľky a testovacie dáta
Nižšie je dátový model mojej aplikácie (tabuľky, stĺpce, vzťahy, pravidlá ON DELETE).
Vyrob z neho dva súbory pre MariaDB 10.6.
1. schema.sql
- Na začiatku DROP TABLE IF EXISTS pre všetky tabuľky v poradí, v akom sa dajú zmazať
(prepojky, potom hlavná entita, potom číselníky).
- CREATE TABLE v poradí, v akom sa dajú vytvoriť (číselníky, hlavná entita, prepojky).
- Každá tabuľka: ENGINE=InnoDB, DEFAULT CHARSET=utf8mb4, COLLATE=utf8mb4_unicode_ci.
- Primárny kľúč: id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY.
- Cudzie kľúče ako FOREIGN KEY ... REFERENCES ... s pravidlom ON DELETE presne podľa modelu.
- Pri každej tabuľke a každom cudzom kľúči krátky SQL komentár (-- ...) po slovensky,
čo tabuľka drží a prečo platí dané pravidlo ON DELETE.
- Nepíš CREATE DATABASE ani USE.
2. seed.sql
- INSERT do každej tabuľky, 3 až 5 riadkov, zmysluplné hodnoty po slovensky k mojej téme.
- Poradie vkladania: číselníky, hlavná entita, prepojky; cudzie kľúče odkazujú na existujúce id.
Dátový model:
[SEM VLOŽ DÁTOVÝ MODEL Z README]
Prečo je prompt napísaný práve takto:
- Poradie mazania a vytvárania je vymenované, lebo AI ho pri prepojkách často preto hodí a
schema.sqlpotom padne na cudzom kľúči. ENGINE,CHARSETa tvar primárneho kľúča sú predpísané, aby všetky tabuľky v skupine vyzerali rovnako a dali sa porovnávať so vzorom.- Komentáre pri ON DELETE žiadame preto, že pri obhajobe vysvetľuješ každé pravidlo.
CREATE DATABASEaUSEzakazujeme, lebo databázu už máš a cudzia by na sigma.tuke.sk nešla vytvoriť.- Seed s reálnymi hodnotami k tvojej téme: stránka s výpisom má o čom hovoriť a pri obhajobe vidno, že dátam rozumieš.
Ukážka 8: prompt na modely
Prikladám kostru PHP projektu (namespace App\Model, autoload PSR-4 cez Composer):
ModelInterface.php, BaseModel.php, LookupModel.php (vzor číselníka), MainModel.php
(vzor hlavnej entity) a svoj schema.sql.
Vyrob pre moje tabuľky triedy modelov, pre každú jeden súbor do src/Model/:
- pre každý číselník triedu podľa vzoru LookupModel: extends BaseModel, nastav $table,
describe() vráti krátky popis po slovensky; nič iné nepridávaj,
- pre hlavnú entitu triedu podľa vzoru MainModel: implements ModelInterface, getAll() a
getById() s JOIN na číselník pripojený vzťahom 1:N (vráť aj jeho name pod zrozumiteľným
aliasom), getCount(), delete(), describe(); prepared statements s pomenovanými
placeholdermi.
Názvy tried: anglicky, jednotné číslo, prípona Model (categories -> CategoryModel).
Každá trieda má PHP 8.1 syntax: declare(strict_types=1), typy parametrov a návratových hodnôt.
Nad každou metódou jednoriadkový komentár po slovensky, čo robí.
Na konci napíš riadky, ktoré mám vložiť do poľa $models v index.php.
[SEM VLOŽ OBSAH ŠTYROCH SÚBOROV KOSTRY A SCHEMA.SQL]
Prečo je prompt napísaný práve takto:
- Prikladáš kostru, nie opis kostry: AI vidí presný
namespace, mená metód a štýl, a výsledok do kostry zapadne bez úprav. - „Nič iné nepridávaj” pri číselníkoch bráni tomu, aby AI do päťriadkovej triedy dopísala vlastné
getAll()a dedenie stratilo zmysel. - Pravidlo pre názvy tried je tu preto, že názov súboru musí sedieť s menom triedy, inak PSR-4 autoloader súbor nenájde.
- PHP 8.1 je uvedené výslovne, lebo AI inak rado použije novšie konštrukcie, ktoré na sigma.tuke.sk nebežia.
- Riadky pre
$modelsna konci ušetria preklepy v plných menách tried.
Example 1: the skeleton structure
The skeleton is available as kostra.zip; unpacked into the project folder it looks like this:
cv02/
├── .htaccess src/, vendor/, sql/ and config.php are not reachable from the web
├── .gitignore config.php, vendor/, .vscode/ do not go into the repository
├── composer.json PSR-4 autoload: App\ -> src/
├── config.example.php configuration example without the password (in the repository)
├── config.php your configuration with the password (only in Kubeflow and on sigma.tuke.sk)
├── index.php check page
├── sql/
│ ├── schema.sql CREATE TABLE for all tables
│ └── seed.sql test data
├── src/
│ ├── Database.php App\Database – one PDO connection
│ └── Model/
│ ├── ModelInterface.php
│ ├── BaseModel.php parent of lookup models
│ ├── LookupModel.php lookup table example (delete once you have your own)
│ └── MainModel.php main entity example (delete once you have your own)
└── vendor/ created by composer install (not in the repository)
└── autoload.php
Example 2: schema.sql of the sample project
The recipe tables from last week, in the order they can be created. Note that DROP TABLE goes in the reverse order of CREATE TABLE.
-- Dropping in reverse order: pivot tables first, then the main entity, lookup tables last.
DROP TABLE IF EXISTS recipe_ingredients;
DROP TABLE IF EXISTS recipe_categories;
DROP TABLE IF EXISTS recipes;
DROP TABLE IF EXISTS units;
DROP TABLE IF EXISTS ingredients;
DROP TABLE IF EXISTS categories;
DROP TABLE IF EXISTS difficulties;
-- Lookup table: recipe difficulty (easy, medium, hard).
CREATE TABLE difficulties (
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(50) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Lookup table: meal category (soup, main course, dessert).
CREATE TABLE categories (
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(50) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Lookup table: ingredient.
CREATE TABLE ingredients (
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(80) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Lookup table: unit of measure (g, ml, pcs).
CREATE TABLE units (
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(30) NOT NULL,
abbreviation VARCHAR(10) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Main entity: recipe.
CREATE TABLE recipes (
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(120) NOT NULL,
servings TINYINT UNSIGNED NOT NULL DEFAULT 4,
prep_time SMALLINT UNSIGNED NOT NULL, -- minutes
instructions TEXT NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
difficulty_id INT UNSIGNED NOT NULL,
-- 1:N to a lookup table. RESTRICT: a difficulty cannot be deleted while a recipe uses it.
FOREIGN KEY (difficulty_id) REFERENCES difficulties(id) ON DELETE RESTRICT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Pivot table without attributes: recipe × category.
CREATE TABLE recipe_categories (
recipe_id INT UNSIGNED NOT NULL,
category_id INT UNSIGNED NOT NULL,
PRIMARY KEY (recipe_id, category_id),
-- CASCADE: when a recipe is deleted, its links go with it.
FOREIGN KEY (recipe_id) REFERENCES recipes(id) ON DELETE CASCADE,
-- RESTRICT: a category cannot be deleted while a recipe uses it.
FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE RESTRICT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Pivot table with attributes: recipe × ingredient, with amount and unit.
CREATE TABLE recipe_ingredients (
recipe_id INT UNSIGNED NOT NULL,
ingredient_id INT UNSIGNED NOT NULL,
amount DECIMAL(8,2) NOT NULL,
note VARCHAR(100) NULL,
unit_id INT UNSIGNED NOT NULL,
PRIMARY KEY (recipe_id, ingredient_id),
FOREIGN KEY (recipe_id) REFERENCES recipes(id) ON DELETE CASCADE,
FOREIGN KEY (ingredient_id) REFERENCES ingredients(id) ON DELETE RESTRICT,
FOREIGN KEY (unit_id) REFERENCES units(id) ON DELETE RESTRICT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
Example 3: composer.json
{
"name": "tuke/it-projekt",
"description": "Semester project – Information Technologies",
"type": "project",
"require": {
"php": ">=8.1"
},
"autoload": {
"psr-4": {
"App\\": "src/"
}
},
"config": {
"platform": {
"php": "8.1.2"
}
}
}
The only thing that changes in it during the semester is require: when we add Twig in week 6, a line is added and composer install downloads it.
Example 4: Database.php – one connection
<?php
declare(strict_types=1);
namespace App;
use PDO;
final class Database
{
private static ?PDO $instance = null;
// Private constructor: new Database() is not possible.
private function __construct()
{
}
public static function getInstance(): PDO
{
if (self::$instance === null) {
$c = require __DIR__ . '/../config.php';
$dsn = "mysql:host={$c['host']};dbname={$c['dbname']};charset=utf8mb4";
self::$instance = new PDO($dsn, $c['user'], $c['pass'], [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]);
}
return self::$instance;
}
}
What each line in Database.php does:
declare(strict_types=1);– PHP will not silently convert types (string to number etc.); a type error shows up immediately. We write it in every file.namespace App;– the class address; the full name isApp\Database, the file issrc/Database.php.use PDO;– PDO is a built-in PHP class without a namespace; since we are inside the namespaceApp, we have to say we mean “the global one”.final class– the class cannot be inherited from; for a singleton that is intentional.private static ?PDO $instance = null;– the only place where the connection lives.static= belongs to the class, not to an object;?PDO= PDO ornull;nullat the start because the connection does not exist yet.private function __construct() {}– private constructor:new Database()from outside ends with an error. This is what makes the class a singleton.if (self::$instance === null)–self::accesses a static property of the class itself. The connection is created only the first time.require __DIR__ . '/../config.php'–__DIR__is the folder of this file (src/),..goes one level up, whereconfig.phpis.requirehere returns the array fromreturn.new PDO($dsn, $c['user'], $c['pass'], [...])– the connection itself. The third and fourth parameters are the user and the password fromconfig.php.ATTR_ERRMODE => ERRMODE_EXCEPTION– an SQL error throws an exception (PDOException), which you see in the browser whendebugis on; without it errors would stay silent.ATTR_DEFAULT_FETCH_MODE => FETCH_ASSOC– rows from the database come as associative arrays ($row['title']), without numeric keys as well.ATTR_EMULATE_PREPARES => false– prepared statements are handled by the database, not by PHP; safer and the types match.return self::$instance;– always the same PDO.
Example 5: ModelInterface and BaseModel
<?php
declare(strict_types=1);
namespace App\Model;
interface ModelInterface
{
public function getAll(): array;
public function getById(int $id): ?array;
public function getCount(): int;
public function delete(int $id): bool;
public function describe(): string;
}
<?php
declare(strict_types=1);
namespace App\Model;
use App\Database;
use PDO;
abstract class BaseModel implements ModelInterface
{
protected PDO $pdo;
protected string $table; // set by every child
public function __construct()
{
$this->pdo = Database::getInstance();
}
public function getAll(): array
{
return $this->pdo->query("SELECT * FROM {$this->table} ORDER BY id")->fetchAll();
}
public function getById(int $id): ?array
{
$stmt = $this->pdo->prepare("SELECT * FROM {$this->table} WHERE id = :id");
$stmt->execute([':id' => $id]);
$row = $stmt->fetch();
return $row === false ? null : $row;
}
public function getCount(): int
{
return (int) $this->pdo->query("SELECT COUNT(*) FROM {$this->table}")->fetchColumn();
}
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;
}
abstract public function describe(): string;
}
What each line in BaseModel.php does:
abstract class BaseModel implements ModelInterface– the parent of lookup models; it commits to the interface but cannot be created itself.protected PDO $pdo;– the connection all methods will use;protectedso that children see it too.protected string $table;– the table name, deliberately without a value: every child fills it in.$this->pdo = Database::getInstance();– in the constructor the model asks for the shared connection. The child does not write a constructor; it inherits this one.getAll():query()runs a query without parameters,fetchAll()returns all rows as an array of arrays.{$this->table}is inserted into the string.getById():prepare()prepares the query with:id,execute([':id' => $id])supplies the value,fetch()returns one row orfalsewhen nothing was found. Hencereturn $row === false ? null : $row;– the method promises?array, i.e. an array ornull, notfalse.getCount():fetchColumn()returns the first column of the first row – withCOUNT(*)that is the number itself;(int)converts it from a string to an integer, because the method promisesint.delete(): afterexecute(),rowCount()says how many rows were deleted;> 0givestrue/false. If another table uses the record withON DELETE RESTRICT, the database refuses the delete and throws an exception – that is correct behaviour, not a bug.abstract public function describe(): string;– without a body; every child must write it.
Example 6: the sample project’s models
A lookup model has five lines of its own code. The main entity does not inherit, because its queries differ: a JOIN adds the difficulty name.
<?php
declare(strict_types=1);
namespace App\Model;
class DifficultyModel extends BaseModel
{
protected string $table = 'difficulties';
public function describe(): string
{
return 'Difficulty lookup table';
}
}
<?php
declare(strict_types=1);
namespace App\Model;
use App\Database;
use PDO;
class RecipeModel implements ModelInterface
{
private PDO $pdo;
public function __construct()
{
$this->pdo = Database::getInstance();
}
// Recipes with the difficulty name, newest first.
public function getAll(): array
{
$sql = "SELECT r.*, d.name AS difficulty_name
FROM recipes r
JOIN difficulties d ON d.id = r.difficulty_id
ORDER BY r.id DESC";
return $this->pdo->query($sql)->fetchAll();
}
public function getById(int $id): ?array
{
$sql = "SELECT r.*, d.name AS difficulty_name
FROM recipes r
JOIN difficulties d ON d.id = r.difficulty_id
WHERE r.id = :id";
$stmt = $this->pdo->prepare($sql);
$stmt->execute([':id' => $id]);
$row = $stmt->fetch();
return $row === false ? null : $row;
}
public function getCount(): int
{
return (int) $this->pdo->query("SELECT COUNT(*) FROM recipes")->fetchColumn();
}
public function delete(int $id): bool
{
$stmt = $this->pdo->prepare("DELETE FROM recipes WHERE id = :id");
$stmt->execute([':id' => $id]);
return $stmt->rowCount() > 0;
}
public function describe(): string
{
return 'Recipes';
}
}
What each line in the models does:
DifficultyModel extends BaseModel– inherits the constructor,$pdoand the four methods.protected string $table = 'difficulties';overrides the parent’s empty property with a value.describe()is the only method it must write, because it is abstract in the parent. The whole class has five lines of its own code – that is the point of inheritance.RecipeModel implements ModelInterface– does not inherit, so it has its ownprivate PDO $pdo;(it may beprivatehere, nobody inherits from it) and its own constructor, which asks for the connection just likeBaseModel.SELECT r.*, d.name AS difficulty_name FROM recipes r JOIN difficulties d ON d.id = r.difficulty_id–randdare table aliases.r.*= all columns of the recipe,d.name AS difficulty_name= plus the difficulty name under a readable name.JOIN … ONpairs the recipe row with the difficulty row whoseidequalsdifficulty_id. In the result$row['difficulty_name']is “medium” instead of the number 2.ORDER BY r.id DESC– newest recipes first; lookup tables useORDER BY id, because insertion order is also display order.getById()with the sameJOINandWHERE r.id = :id– with aliases you must say which table’sidyou mean.getCount()anddelete()are the same as inBaseModel, only with a fixed table name.
In index.php the models are then used the same way, whether they inherit or not:
$models = [
new \App\Model\DifficultyModel(),
new \App\Model\CategoryModel(),
new \App\Model\RecipeModel(),
];
What the skeleton’s index.php (the check page) does:
require __DIR__ . '/vendor/autoload.php';– the onlyrequirein the application; from this line on PHP finds everyApp\…class itself.$config = require __DIR__ . '/config.php';andif ($config['debug']) { ini_set('display_errors', '1'); error_reporting(E_ALL); }– sigma.tuke.sk has error display switched off; during development we want to see errors, so we switch them on whendebugistruein the configuration.use App\Database;and$pdo = Database::getInstance();– the first and only creation of the connection; the models get the same one.$pdo->query('SHOW TABLES')->fetchAll(PDO::FETCH_COLUMN)– the list of tables in the database as a plain array of names; works even before you have any model.$models = [ new \App\Model\CategoryModel(), … ];– the list of your models; you add one line per class. The full name with a leading\becauseindex.phpis in no namespace and we useduseonly forDatabase.foreach ($models as $model)– for every modeldescribe(),getCount()and the first five rows ofgetAll()viaarray_slice. The table header comes fromarray_keys($rows[0])– the column names of the first row.htmlspecialchars(...)at every output – values from the database are not printed directly, so that a<in the data cannot break the page; protection against XSS, in more detail in week 6.
Example 7: the prompt for tables and test data
Below is the data model of my application (tables, columns, relationships, ON DELETE rules).
Create two files from it for MariaDB 10.6.
1. schema.sql
- At the beginning DROP TABLE IF EXISTS for all tables in the order they can be dropped
(pivot tables, then the main entity, then lookup tables).
- CREATE TABLE in the order they can be created (lookup tables, main entity, pivot tables).
- Every table: ENGINE=InnoDB, DEFAULT CHARSET=utf8mb4, COLLATE=utf8mb4_unicode_ci.
- Primary key: id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY.
- Foreign keys as FOREIGN KEY ... REFERENCES ... with the ON DELETE rule exactly as in the model.
- A short SQL comment (-- ...) at every table and every foreign key saying what the table holds
and why the given ON DELETE rule applies.
- Do not write CREATE DATABASE or USE.
2. seed.sql
- INSERT into every table, 3 to 5 rows, meaningful values for my topic.
- Insert order: lookup tables, main entity, pivot tables; foreign keys refer to existing ids.
Data model:
[PASTE THE DATA MODEL FROM YOUR README HERE]
Why the prompt is written this way:
- The drop and create order is spelled out, because the AI often gets it wrong with pivot tables and
schema.sqlthen fails on a foreign key. ENGINE,CHARSETand the primary key form are prescribed so that all tables in the group look the same and can be compared with the sample.- We ask for comments at ON DELETE because you explain every rule at the defence.
CREATE DATABASEandUSEare forbidden because you already have a database and another one could not be created on sigma.tuke.sk.- Seed data with real values for your topic: the listing page has something to show and at the defence it is clear you understand the data.
Example 8: the prompt for models
Attached is the skeleton of a PHP project (namespace App\Model, PSR-4 autoload via Composer):
ModelInterface.php, BaseModel.php, LookupModel.php (lookup table example), MainModel.php
(main entity example) and my schema.sql.
Create model classes for my tables, one file each into src/Model/:
- for every lookup table a class following LookupModel: extends BaseModel, set $table,
describe() returns a short description; add nothing else,
- for the main entity a class following MainModel: implements ModelInterface, getAll() and
getById() with a JOIN to the lookup table connected by the 1:N relationship (return its name
under a readable alias), getCount(), delete(), describe(); prepared statements with named
placeholders.
Class names: English, singular, suffix Model (categories -> CategoryModel).
Every class uses PHP 8.1 syntax: declare(strict_types=1), parameter and return types.
A one-line comment above every method saying what it does.
At the end write the lines I should add to the $models array in index.php.
[PASTE THE CONTENTS OF THE FOUR SKELETON FILES AND SCHEMA.SQL HERE]
Why the prompt is written this way:
- You attach the skeleton, not a description of it: the AI sees the exact
namespace, method names and style, and the result fits the skeleton without changes. - “Add nothing else” for lookup tables stops the AI from writing its own
getAll()into a five-line class, which would make inheritance pointless. - The class naming rule is there because the file name must match the class name, otherwise the PSR-4 autoloader will not find the file.
- PHP 8.1 is stated explicitly because the AI otherwise likes newer constructs that do not run on sigma.tuke.sk.
- The
$modelslines at the end save typos in the full class names.
Úlohy na cvičenieLab tasks
Úlohy dostaneš aj ako zadanie cv02 v Classroom 50, sú v súbore README.md v tvojom repository. Repository má na začiatku len README.md; kostru doň pridáš v úlohe 3.2.
Link na zadanie cv02: prijať zadanie (platí pre skupiny B. Stehlíkovej; ostatné skupiny dostanú link od svojho cvičiaceho)
Úloha 3.1: Kubeflow: príprava. Podľa návodu 04 Odovzdávanie, časť 1: otvor Kubeflow, skontroluj, že v Explorer je hore WEB, a skontroluj rozšírenia. Ak dole na stavovej lište nie je Go Live, rozšírenia sa po reštarte stratili. Vráti ich tento príkaz v termináli (jeden riadok):
code-server --install-extension Natizyskunk.sftp --install-extension yandeu.five-server --install-extension bmewburn.vscode-intelephense-client --install-extension mblode.twig-language-2
Potom F1, napíš reload, Developer: Reload Window.
Úloha 3.2: Clone, kostra a nastavenie. Prijmi zadanie cv02 (link je vyššie) a repository naklonuj do ~/web/it/cv02 podľa návodu 04 Odovzdávanie, časti 2 a 3 (posledné slovo príkazu je cv02). Repository má zatiaľ len README.md; kostru stiahneš z webu predmetu a rozbalíš do neho. V termináli:
cd ~/web/it/cv02
curl -O https://beata.stehlikova.website.tuke.sk/it/kostra.zip
unzip kostra.zip && rm kostra.zip
composer install
cp config.example.php config.php
V Explorer sa objavia súbory kostry (.htaccess a .gitignore začínajú bodkou, Explorer ich ukáže sivšie). Otvor config.php v editore a doplň dbname, user a pass (heslo z cvičenia). Ulož. Potom pravý klik na priečinok cv02, Upload Folder – tým sa na sigma.tuke.sk dostane aj vendor/, ktorý neprešiel editorom. Kontrola: https://sigma.tuke.sk/student/meno.priezvisko/it/cv02/ ukáže stránku „Kontrola projektu” s 0 tabuľkami.
Úloha 3.3: Tabuľky a testovacie dáta cez AI. Prompt z Ukážky 7 vlož do AI spolu s dátovým modelom zo svojho README z cv01. Výsledok ulož ako sql/schema.sql a sql/seed.sql. Prečítaj si každý ON DELETE, musí sedieť s tvojím modelom. V Adminer klikni vľavo SQL command, vlož obsah schema.sql, Execute; to isté so seed.sql. Kontrola: v Adminer vidíš svoje tabuľky s dátami a stránka „Kontrola projektu” ich vypíše v zozname.
Úloha 3.4: Modely cez AI. Prompt z Ukážky 8 vlož do AI spolu so súbormi src/Model/ModelInterface.php, BaseModel.php, LookupModel.php, MainModel.php a svojím sql/schema.sql. Vzniknuté triedy ulož do src/Model/ (jedna trieda = jeden súbor, názov súboru = názov triedy). Vzorové LookupModel.php a MainModel.php potom zmaž. Prečítaj každú metódu, pri obhajobe ju vysvetlíš.
Úloha 3.5: Kontrolná stránka. V index.php doplň do poľa $models jeden riadok new \App\Model\TvojModel(), pre každú svoju triedu. Ulož a otvor stránku na sigma.tuke.sk: pri každom modeli vidíš popis, počet záznamov a prvých 5 riadkov. Pri hlavnej entite musí byť aj stĺpec s názvom z číselníka (JOIN). Ak stránka hlási chybu, prečítaj ju – debug v config.php je zapnutý práve preto.
Úloha 3.6: Odovzdanie. Commit a push podľa návodu 04 Odovzdávanie, časť 5. config.php a vendor/ do repository nejdú (sú v .gitignore), to je správne. Na GitHub skontroluj, že je tam celá kostra, sql/, src/Model/ s tvojimi triedami a index.php.
Úloha 3.7: Kontrola na sigma.tuke.sk. Pravý klik na cv02, Upload Folder, potom otvor https://sigma.tuke.sk/student/meno.priezvisko/it/cv02/. Over aj, že …/it/cv02/src/ a …/it/cv02/config.php vrátia chybu 404 alebo 403 – kód a heslo nie sú z webu prístupné.
Hodnotenie: 1 bod. Vlastné tabuľky s testovacími dátami v databáze, modely v repository a kontrolná stránka vo webovom priestore na sigma.tuke.sk, ktorá vypíše záznamy z každej tabuľky. Commit a push najneskôr do nasledujúceho cvičenia.
You also get the tasks as the assignment cv02 in Classroom 50; they are in the file README.md in your repository. The repository has only README.md at first; you add the skeleton in task 3.2.
Link to the assignment cv02: accept the assignment (applies to B. Stehlíková’s groups; other groups get the link from their own lab teacher)
Task 3.1: Kubeflow: getting ready. Following guide 04 Submitting, part 1: open Kubeflow, check that Explorer shows WEB at the top, and check the extensions. If the status bar at the bottom does not show Go Live, the extensions were lost on restart. This command in the terminal (one line) brings them back:
code-server --install-extension Natizyskunk.sftp --install-extension yandeu.five-server --install-extension bmewburn.vscode-intelephense-client --install-extension mblode.twig-language-2
Then F1, type reload, Developer: Reload Window.
Task 3.2: Clone, skeleton and setup. Accept the assignment cv02 (link above) and clone the repository into ~/web/it/cv02 following guide 04 Submitting, parts 2 and 3 (the last word of the command is cv02). The repository has only README.md so far; you download the skeleton from the course website and unpack it into the repository. In the terminal:
cd ~/web/it/cv02
curl -O https://beata.stehlikova.website.tuke.sk/it/kostra.zip
unzip kostra.zip && rm kostra.zip
composer install
cp config.example.php config.php
The skeleton files appear in Explorer (.htaccess and .gitignore start with a dot; Explorer shows them greyed). Open config.php in the editor and fill in dbname, user and pass (the password from the lab). Save. Then right-click the cv02 folder, Upload Folder – this gets vendor/, which did not pass through the editor, to sigma.tuke.sk as well. Check: https://sigma.tuke.sk/student/name.surname/it/cv02/ shows the “Kontrola projektu” page with 0 tables.
Task 3.3: Tables and test data with AI. Paste the prompt from Example 7 into an AI together with the data model from your cv01 README. Save the result as sql/schema.sql and sql/seed.sql. Read every ON DELETE; it must match your model. In Adminer click SQL command on the left, paste the contents of schema.sql, Execute; the same with seed.sql. Check: Adminer shows your tables with data and the “Kontrola projektu” page lists them.
Task 3.4: Models with AI. Paste the prompt from Example 8 into an AI together with the files src/Model/ModelInterface.php, BaseModel.php, LookupModel.php, MainModel.php and your sql/schema.sql. Save the generated classes into src/Model/ (one class = one file, file name = class name). Then delete the example LookupModel.php and MainModel.php. Read every method; you will explain it at the defence.
Task 3.5: Check page. In index.php add one line new \App\Model\YourModel(), to the $models array for each of your classes. Save and open the page on sigma.tuke.sk: for every model you see the description, the record count and the first 5 rows. The main entity must also show the column with the name from the lookup table (JOIN). If the page reports an error, read it – debug in config.php is on exactly for this.
Task 3.6: Submitting. Commit and push following guide 04 Submitting, part 5. config.php and vendor/ do not go into the repository (they are in .gitignore); that is correct. On GitHub check that the whole skeleton, sql/, src/Model/ with your classes and index.php are there.
Task 3.7: Check on sigma.tuke.sk. Right-click cv02, Upload Folder, then open https://sigma.tuke.sk/student/name.surname/it/cv02/. Also check that …/it/cv02/src/ and …/it/cv02/config.php return a 404 or 403 error – the code and the password are not reachable from the web.
Grading: 1 point. Your own tables with test data in the database, models in the repository and the check page in your web space on sigma.tuke.sk listing the records of every table. Commit and push by the next lab at the latest.