Konvence kódu#
Jak má vypadat soubor, který v tomhle CMS napíšete. Kanonické a závazné znění je ve
skillech skills/code-standard/ — tahle kapitola je z nich udělaný návod: kostra
souboru, pojmenování a postupy pro to, co se zakládá nejčastěji.
Platí to pro každý PHP soubor v App\ — presenter, komponentu, entitu, manager
i trait. Vrstvová pravidla (co smí volat co) řeší Pětivrstvý model; tahle
stránka je o zápisu samotného souboru.
Kdy podle toho jet a kdy ne
Nový soubor píšete kanonicky vždy. Cizí soubor sjednocujete jen v rozsahu, který stejně upravujete — plošné přepisování starých souborů „při té příležitosti“ dělá code review nečitelnou a nikdo pak nepozná, co je oprava a co kosmetika.
Kostra souboru#
<?php
declare(strict_types=1);
namespace App\UI\Front\Blog\Presenters;
use Nette\Application\Attributes\Persistent; // 1. Nette
use DateTime; // 2. ostatní vendor + PHP
use Doctrine\ORM\Mapping as ORM;
use App\Core\Forms\Form; // 3. App\
use App\UI\Front\Blog\Models\Managers\NewsManager;
final class NewsPresenter extends BasePresenter
{
use NewsInitTrait;
use NewsListTrait;
protected ?string $translatorDomain = 'front.blog.presenters.news';
#[Persistent]
public int $id;
public function __construct(
private readonly NewsManager $newsManager,
) {
parent::__construct();
}
}
| Prvek | Pravidlo |
|---|---|
| Hlavička | tři řádky — <?php, prázdný, declare, prázdný, namespace. Ne jednořádkové <?php declare(...) |
use |
tři skupiny oddělené prázdným řádkem, uvnitř abecedně, žádný nepoužitý |
| Třída | listová třída final; abstract jen v App\Core\Base |
| Properties | plně typované, konkrétně pojmenované ($regionManager, ne $manager) |
| Konstruktor | promované private readonly, jeden na řádek, čárka i u posledního |
| Odsazení | tabulátory; { u třídy a metody na samostatném řádku, u if/foreach na témže |
| Komentáře | anglicky, krátce, vysvětlují proč — ne co |
Kolik z toho už platí v kódu
Změřeno na 4 833 souborech v app/: 3 917 má declare na samostatném řádku,
861 ještě starou jednořádkovou variantu. abstract je mimo Core/Base
v 15 souborech. Čísla se dají přepočítat:
grep -rl "^declare(strict_types=1);" app --include="*.php" | wc -l.
Prefix metody říká, co dělá#
Nejde o kosmetiku — z prefixu se pozná, jestli metoda vrací data, nebo sahá na stav. Nesoulad je nejčastější důvod, proč se v cizím presenteru nedá vyznat.
| Prefix | Co dělá | Vrací |
|---|---|---|
init<X> |
naplní stav objektu ($this->…); volá se ze startup() / setup() / action* |
void |
prepare<X> |
připraví data pro šablonu ($this->template->…); volá se z render* |
void / data |
find<X> |
dotaz do úložiště přes manager | entita / kolekce |
load<X> |
načte a uloží do stavu (lazy cache) | načtené |
resolve<X> |
vybere správnou hodnotu z víc zdrojů nebo z kontextu | hodnota |
get<X> / set<X> |
accessor nad už existující hodnotou | hodnota / void|self |
is<X> / has<X> / can<X> |
čistý predikát bez vedlejších efektů | bool |
build<X> / create<X> |
sestaví novou strukturu — DTO, entitu, pole | objekt / pole |
check<X> |
složený guard — vyhodnotí predikáty a zařídí tok (redirect, error, setView) |
void / bool |
handle<X> |
výhradně signál, nikdy běžný helper | void |
save / delete / update |
perzistence přes manager | Result |
Metody se v souboru řadí podle role a toku, ne abecedně: lifecycle → dvojice
action*+render* → createComponent* → handle* → on* → privátní pomocné dole.
handle* použité pro helper udělá z metody veřejný signál
Nette namapuje handle* na URL ?do=…. Pomocná metoda pojmenovaná handleFoo()
je tím pádem volatelná zvenčí kýmkoli s odkazem — a nikdo si toho nevšimne,
protože v aplikaci se volá normálně jako metoda.
Postup: zakládám nový presenter#
- Založ složku
app/UI/<Sekce>/<Modul>/Presenters/a v níXxxPresenter.php. - Soubor presenteru nech prázdný na logiku — jen
final,extends BasePresenter,$translatorDomain,#[Persistent]properties, konstruktor sparent::__construct()a seznamusetraitů. - Logiku rozděl do traitů podle role, ne podle velikosti:
Presenters/Traits/
Inits/XxxInitTrait.php — startup, sdílená příprava, signály daného tématu
Lists/XxxListTrait.php — actionList / renderList
Details/XxxDetailTrait.php — actionDetail / renderDetail
Forms/XxxFormTrait.php — createComponentXxxForm
- Rozděl
action*arender*.action*načte data do private property a udělá validace ($this->error(),redirect(),setView()).render*už jen čte z property a plní$this->template. - Signatury dvojice srovnej —
actionDetail(int $id)arenderDetail(int $id)musí mít stejné parametry ve stejném pořadí a plně otypované. - Závislosti: presenter → konstruktor. Trait do konstruktoru nemůže, takže
použije
#[Inject]public property v traitu — jediná povolená výjimka z constructor injection. - Meta a OG data naplň v presenteru do
$this->metaData/$this->ogData, ne v šabloně.
Dotaz do databáze v render* se zopakuje při každém AJAX překreslení
render* běží i při redrawControl(). Dotaz, který v action* proběhl jednou,
se odtud pošle znovu při každém signálu — a protože stránka funguje, přijde se na
to až podle zátěže databáze, ne podle chyby.
Postup: zakládám novou komponentu#
- Trojice se stejným základem názvu: složka
XxxComponent/, třídaXxx(bez suffixu,final), factoryXxxFactory. - Šablony vždy do
Templates/Default/— i komponenta s jedinou šablonou. Prázdný stav jeEmpty.latte. - Šablonu nastav přes
setTemplateFile(__DIR__, $file), nikdysetFile()ručně. Base za tebe zkusíTemplates/<Modul tématu>/<Varianta>/, pakTemplates/<Modul tématu>/default/a teprve pakTemplates/Default/. - Factory jen poskládá a zavolá
initControl():
final class RegionListFactory extends BaseComponentFactory
{
public function __construct(
private readonly RegionManager $regionManager,
) {}
public function create(): RegionList
{
$this->control = new RegionList($this->regionManager);
$this->initControl();
return $this->control;
}
}
- Factory nebere
AppContexta nevoláparent::__construct(). Kontext do ní vstříkne DI dekorátor (config/Shared/decorators.neon→setAppContext()). - Init rozděl podle toho, co potřebuje:
| Metoda | Kdy běží | K čemu |
|---|---|---|
__construct |
sestavení ve factory | DI |
onCreate() |
z initControl() po sestavení |
init nezávislý na presenteru |
setup() |
při připojení k presenteru | analog startup() — potřebuje request |
prepare*() z render* |
při vykreslení | data závislá na argumentech render* |
- Varianty vzhledu pojmenuj věcně (
Box,Row,Grid,Inline), ne číslem, pokud varianta nějaký význam má. Volají se{control frontBazaarGallery:box}.
Vlastní setup() bez parent::setup($presenter) rozbije překlady a šablonu
Base setup() navěšuje translator, $this->template, router a DI kontejner.
Když ho override zapomene zavolat, komponenta se vykreslí — jen bez přeložených
textů a s prázdným $this->template->lang. Vypadá to jako chybějící překladový
klíč, ne jako chyba v PHP.
renderStyle1() se nepíše
BaseComponent::__call z renderXxx() udělá render('Xxx') sám. Ručně napsaná
renderStyle1() funguje taky, ale rozejde se s ostatními komponentami —
a __call navíc mezi písmeno a číslici doplní podtržítko (renderStyle1 →
soubor Style_1.latte), takže ruční varianta hledá jiný soubor než automatická.
Postup: zakládám entitu a manager#
Entita v tomhle systému existuje ve čtyřech podobách. Ověřitelně: News je
app/Core/Base/Shared/Blog/… (abstraktní předek) + Api/, Admin/ a Front/
varianta nad toutéž tabulkou cms_mod_blog_news.
- Sdílený předek do
app/Core/Base/Shared/<Modul>/Models/Entities/—abstract,#[ORM\MappedSuperclass]+#[TableAlias('alias')],protectedtypované sloupce, gettery a settery. - Konkrétní varianty do
app/UI/<Sekce>/<Modul>/Models/Entities/—#[ORM\Entity] #[ORM\Table(name: '…')], dědí ze sdíleného předka a přidávají jen asociace, které ta sekce opravdu potřebuje.- Kolekce vždy přes per-collection trait
(
Core/Traits/Shared/Models/Entities/Collections/), inicializace v konstruktoru předka. - Identita přes
IdentifiableTrait→getId(). NikdygetNewsId(). - Manager nech tenký — jen předá service a nese generikum v PHPDoc:
/**
* @extends BaseManager<NewsService>
*/
class NewsManager extends BaseManager
{
public function __construct(NewsService $service)
{
parent::__construct($service);
}
}
- Čtení je fluentní a končí
->execute():
$entity = $this->newsManager->find($id)->include(['translations', 'image'])->execute();
$items = $this->newsManager->findAll($filters, $orderBy, $offset, $limit)->execute();
- Zápis si musí manager vyžádat. Front base manager umí jen číst — má
GetTrait,GetByTrait,GetAllTrait,TotalCountTraita nic víc. Kdo potřebuje ukládat, přibere si traity explicitně:
use App\Core\Traits\Shared\Models\Managers\DeleteTrait;
use App\Core\Traits\Shared\Models\Managers\SaveTrait;
class AdvertFollowerManager extends BaseManager
{
use SaveTrait;
use DeleteTrait;
}
Dělá to 25 ze 111 frontových managerů (grep -rl "SaveTrait\|DeleteTrait"
app/UI/Front --include="*Manager.php" | wc -l). Admin a Api base manager mají
zápis rovnou v sobě.
- Vedlejší efekty zápisu (denormalizace, přepočty, notifikace) dej do lifecycle hooků manageru, ne do presenteru — viz Životní cyklus manageru.
Doctrine entita nesmí být final
Doctrine si nad entitou staví proxy třídu; final to znemožní a spadne to až za
běhu při lazy loadu, ne při sestavení. V kódu je to dodržené beze zbytku —
z 848 tříd s #[ORM\Entity] není final ani jedna.
Accessor kolekce se musí jmenovat podle property
protected Collection $translations potřebuje getTranslations(). Při nesouladu
nespadne nic — kolekce se tiše nenačte, API ji neserializuje a admin formulář ji
pak uloží prázdnou. Projeví se to jako „zmizely překlady“, ne jako chyba.
Postup: rozkládám velký soubor do traitů#
- Najdi hranici podle role, ne podle počtu řádků: výpis, detail, formulář, signály jednoho tématu.
- Trait vázaný na jednu třídu dej do podsložky
Traits/u té třídy. - Trait sdílený víc třídami dej do
app/Core/Traits/, členěno podle vrstvy naShared//Api//Admin//Front//Cron/a dál podle druhu naPresenters//Components//Models/. - Trait má stejnou hlavičku jako každý jiný soubor a namespace končící
\Traits\…. - V původní třídě nech jen properties, konstruktor a vstupní metody.
Obecný trait ve složce konkrétní třídy najde druhý člověk až kopií
Když je sdílitelný trait schovaný v Presenters/Traits/ jednoho presenteru,
další vývojář ho nenajde a napíše si vlastní. Rozdíl mezi kopiemi se ukáže až ve
chvíli, kdy se jedna z nich opraví.
Postup: upravuji cizí soubor#
- Sjednoť to, čeho se stejně dotýkáš — hlavičku,
use, typy u metod, které měníš. - Nesahej na to, co s úpravou nesouvisí. Přejmenování variant šablon nebo plošná změna setterů patří do vlastního commitu.
- Starý formulářový vzor neupravuj.
setTranslator+setTranslatorDomain $t = $form->getTranslateFnc()zůstává funkční; kanonicky se píšou jen nové formuláře (viz Formuláře).- Odstraň, co tam nemá být: nepoužité
use, zakomentovaný kód,bdump()/dump()/dd(). Ladicí volání hlídá pre-commit hook. - Prožeň PHPStanem — viz Testy a nástroje.
Anti-vzory, které se v kódu ještě najdou#
| Co uvidíte | Cílový tvar |
|---|---|
<?php declare(strict_types=1); na jednom řádku |
tři řádky |
OgDataDTO |
akronym jako slovo — OgDataDto |
protected $apiEndpoint bez typu |
plně typovaná property |
InitializationTrait, RenderInitTrait |
InitTrait |
Style_1.latte |
Style1.latte, lépe pojmenovaná varianta Box.latte |
parts/ jako složka partialů |
Sections/ / Elements/ / Items/ podle obsahu |
{define metaTitle} v šabloně |
$metaData / $ogData v presenteru |
Přejmenování variant šablon je jeden atomický krok, ne průběžný úklid
Podtržítkový tvar Style_1.latte je v kódu ve 136 souborech
(find app -name "Style_*.latte" | wc -l), bezpodtržítkový v jednom. Přejmenovat
jeden soubor bez úpravy BaseComponent::__call a všech volání znamená, že se
šablona nenajde a komponenta vykreslí prázdno — bez chybové hlášky.
Kam sáhnout#
| Chci | Kde |
|---|---|
| závazné znění konvence | skills/code-standard/cs-conv-*/SKILL.md |
| univerzální PHP základ | skills/code-standard/cs-conv-php/SKILL.md |
| base třídy, ze kterých se dědí | app/Core/Base/ |
| sdílené traity | app/Core/Traits/{Shared,Api,Admin,Front,Cron}/ |
| jádro formulářů | app/Core/Forms/ |
| postupný průchod systémem a jeho stav | skills/code-standard/cs-sweep/SKILL.md |
Navazující kapitoly: Pětivrstvý model · Struktura projektu · Komponenty · Formuláře · Šablony Latte