Přeskočit obsah
V
Pro vývojáře
Architektura, konvence, jádro systému a bezpečnost
Začínáme / Konvence zápisu kódu

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 917declare 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#

  1. Založ složku app/UI/<Sekce>/<Modul>/Presenters/ a v ní XxxPresenter.php.
  2. Soubor presenteru nech prázdný na logiku — jen final, extends BasePresenter, $translatorDomain, #[Persistent] properties, konstruktor s parent::__construct() a seznam use traitů.
  3. 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
  1. Rozděl action* a render*. 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.
  2. Signatury dvojice srovnejactionDetail(int $id) a renderDetail(int $id) musí mít stejné parametry ve stejném pořadí a plně otypované.
  3. 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.
  4. 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#

  1. Trojice se stejným základem názvu: složka XxxComponent/, třída Xxx (bez suffixu, final), factory XxxFactory.
  2. Šablony vždy do Templates/Default/ — i komponenta s jedinou šablonou. Prázdný stav je Empty.latte.
  3. Šablonu nastav přes setTemplateFile(__DIR__, $file), nikdy setFile() ručně. Base za tebe zkusí Templates/<Modul tématu>/<Varianta>/, pak Templates/<Modul tématu>/default/ a teprve pak Templates/Default/.
  4. 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;
    }
}
  1. Factory nebere AppContext a nevolá parent::__construct(). Kontext do ní vstříkne DI dekorátor (config/Shared/decorators.neonsetAppContext()).
  2. 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*
  1. 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.

  1. Sdílený předek do app/Core/Base/Shared/<Modul>/Models/Entities/abstract, #[ORM\MappedSuperclass] + #[TableAlias('alias')], protected typované sloupce, gettery a settery.
  2. Konkrétní varianty do app/UI/<Sekce>/<Modul>/Models/Entities/#[ORM\Entity]
  3. #[ORM\Table(name: '…')], dědí ze sdíleného předka a přidávají jen asociace, které ta sekce opravdu potřebuje.
  4. Kolekce vždy přes per-collection trait (Core/Traits/Shared/Models/Entities/Collections/), inicializace v konstruktoru předka.
  5. Identita přes IdentifiableTraitgetId(). Nikdy getNewsId().
  6. 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);
    }
}
  1. Čtení je fluentní a končí ->execute():
$entity = $this->newsManager->find($id)->include(['translations', 'image'])->execute();
$items  = $this->newsManager->findAll($filters, $orderBy, $offset, $limit)->execute();
  1. Zápis si musí manager vyžádat. Front base manager umí jen číst — má GetTrait, GetByTrait, GetAllTrait, TotalCountTrait a 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ě.

  1. 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ů#

  1. Najdi hranici podle role, ne podle počtu řádků: výpis, detail, formulář, signály jednoho tématu.
  2. Trait vázaný na jednu třídu dej do podsložky Traits/ u té třídy.
  3. Trait sdílený víc třídami dej do app/Core/Traits/, členěno podle vrstvy na Shared/ / Api/ / Admin/ / Front/ / Cron/ a dál podle druhu na Presenters/ / Components/ / Models/.
  4. Trait má stejnou hlavičku jako každý jiný soubor a namespace končící \Traits\….
  5. 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#

  1. Sjednoť to, čeho se stejně dotýkáš — hlavičku, use, typy u metod, které měníš.
  2. Nesahej na to, co s úpravou nesouvisí. Přejmenování variant šablon nebo plošná změna setterů patří do vlastního commitu.
  3. Starý formulářový vzor neupravuj. setTranslator + setTranslatorDomain
  4. $t = $form->getTranslateFnc() zůstává funkční; kanonicky se píšou jen nové formuláře (viz Formuláře).
  5. Odstraň, co tam nemá být: nepoužité use, zakomentovaný kód, bdump() / dump() / dd(). Ladicí volání hlídá pre-commit hook.
  6. 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