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

Komentáře v kódu#

Jak vypadá komentář v PHP souboru tohoto CMS. Závazné znění je ve skillu skills/code-standard/cs-comments/SKILL.md; tahle stránka ukazuje cílovou podobu na dvou celých souborech a shrnuje pravidla, která v nich uvidíte.

Komentáře se píší anglicky, jako dokumentace veřejného API knihovny (vzor: Nette Framework). Z docbloků se generuje referenční dokumentace, proto mají pevný tvar. Tabulky na této stránce jsou česky, protože popisují pravidla, ne kód.

Starší soubory ještě vypadají jinak

Kód se do této podoby převádí po dávkách. Soubor s českými nebo dlouhými komentáři není chyba k opravě „při té příležitosti“ — sjednocují se celé složky najednou, aby se dalo poznat, co je přepis komentářů a co změna kódu.

Pět zásad#

  1. Komentář je kontrakt, ne kronika. Říká, co věc je, co zaručuje a co ji rozbije. Kdy, kdo a proč to měnil, říká git log.
  2. Kód říká co, komentář říká proč. Komentář, který jde přečíst z názvu metody nebo z dalšího řádku, neexistuje.
  3. Jeden styl všude. Stejná kostra, značky, slovník a délka v Core, Api, Admin, Front i Cron. Liší se jen kolik se komentuje: jádro je knihovna s veřejným API, UI je aplikační kód, který volá framework podle konvence.
  4. Dlouhý výklad patří do dokumentace. Docblok nese shrnutí a odkaz; architektura, tabulky a plány žijí v manuálu.
  5. Co generátor nevykreslí, v docbloku není. Odstavce, seznamy, backticks a bloky kódu ano; nadpisy a tabulky ne (v IDE se rozpadnou), emoji ne.

Kdy docblok psát#

Napište docblok, když by čtenář bez něj musel otevřít tělo metody, aby pochopil kontrakt. Jinak ne. Kostru souboru (declare(strict_types=1), final, tři skupiny use) určují Konvence kódu.

Metoda Docblok
public v jádru vždy: shrnutí + kontrakt — z jádra se generuje reference
public v UI když jméno neříká vše
protected když je určena potomkům nebo mění zděděné chování
private jen když účel nebo algoritmus není zřejmý
metoda rozhraní vždy: popisuje očekávané chování, ne jen signaturu
přepsaná metoda bez docbloku, pokud kontrakt nemění — dědí se sám; jinak jen odlišnosti. {@inheritDoc} se nepíše
konstruktor, getter, setter ne; výjimkou je netriviální podmínka inicializace
action*, render*, handle*, createComponent* ne, chování dává konvence Nette; ano, když dělá něco neobvyklého: kontrolu oprávnění, přesměrování, vedlejší účinek

Ukázka: presenter#

Smyšlený administrační presenter. Předvádí kostru souboru, docblok třídy, komentáře u traitů a vlastností, oddělovače sekcí a tři značky.

<?php

declare(strict_types=1);

namespace App\Examples\Comments\Presenters;

use Nette\Application\Attributes\Persistent;

use App\Core\Base\Admin\BasePresenter;
use App\Core\Traits\Admin\Presenters\AdminEditFormComponentsTrait;
use App\Core\Traits\Admin\Presenters\AdminFormHelpersTrait;
use App\Core\Utils\Filters\EqualFilter;
use App\Examples\Comments\Models\Entities\Zone;
use App\Examples\Comments\Models\Managers\ZoneManager;
use App\Examples\Comments\Presenters\Traits\Forms\ZoneFormTrait;
use App\UI\Admin\System\Components\DataGridComponent\DataGrid;
use App\UI\Admin\System\Components\DataGridComponent\DataGridFactory;

/**
 * Warehouse zone administration: list, add and edit (`/admin/store/zone`).
 *
 * A zone groups stock locations of one warehouse. Deleting a zone is a soft delete;
 * locations keep their zone reference so that stock documents stay consistent.
 */
final class ZonePresenter extends BasePresenter
{
    use ZoneFormTrait;                // createComponentEditForm(), saving
    use AdminFormHelpersTrait;        // getFormLanguages(), str()
    use AdminEditFormComponentsTrait; // createComponentAdminLangNav(), createComponentAdminFormActions()

    protected string $translatorDomain = 'admin.store.presenters.zone';

    /** Warehouse the list is filtered by; 0 means all warehouses. */
    #[Persistent]
    public int $warehouseId = 0;

    /** Edited zone (null when adding). */
    private ?Zone $editedZone = null;

    public function __construct(
        private readonly DataGridFactory $dataGridFactory,
        private readonly ZoneManager $zoneManager,
    ) {
        parent::__construct();
    }

    // =========================================================================
    // Actions
    // =========================================================================

    public function actionAdd(): void
    {
        // IMPORTANT: server-side gate. The action is reachable by a typed URL even without
        // the privilege; checkCurrentPrivilege() derives the key from the presenter and
        // action name, so a typo cannot bypass it.
        $this->checkCurrentPrivilege();
    }

    public function actionEdit(int $id): void
    {
        $this->checkCurrentPrivilege();
        $this->editedZone = $this->zoneManager->find($id)->include(['translations'])->execute();
        if ($this->editedZone === null) {
            $this->error();
        }
    }

    public function renderEdit(int $id): void
    {
        $this->template->zone = $this->editedZone;
    }

    // =========================================================================
    // Components
    // =========================================================================

    protected function createComponentGrid(): DataGrid
    {
        $grid = $this->dataGridFactory->create();
        $grid->setDataSource($this->zoneManager->findAll($this->buildListFilters()));

        // NOTE: the warehouse column is hidden on purpose when the list is filtered by one
        // warehouse; it would repeat the same value in every row.
        $grid->addColumnText('warehouse', 'columns.warehouse')
            ->setVisible($this->warehouseId === 0);

        return $grid;
    }

    // =========================================================================
    // Helpers
    // =========================================================================

    /**
     * @return list<EqualFilter>
     */
    private function buildListFilters(): array
    {
        if ($this->warehouseId === 0) {
            return [];
        }

        // TODO: move the filter into ZoneManager::findByWarehouse() once the Front list
        // needs the same query.
        return [new EqualFilter('warehouse', $this->warehouseId)];
    }
}

Ukázka: třída jádra#

Smyšlená služba z App\Core. Předvádí docblok třídy s blokem Usage:, popisek skupiny konstant, @var s tvarem pole, docbloky metod s popisem parametrů, návratové hodnoty a výjimky, zastaralou metodu, a také metody, které docblok nepotřebují.

<?php

declare(strict_types=1);

namespace App\Examples\Comments\Core;

use Nette\Utils\DateTime;

use Doctrine\DBAL\Connection;

use App\Core\Utils\Clock;
use App\Examples\Comments\Core\Exceptions\LockException;

/**
 * Named locks for operations that must not run twice at the same time.
 *
 * A lock is a database row keyed by name. It is held by the request that created it and
 * released on `release()` or at the end of the request, whichever comes first. Locks are
 * fail-open: when the lock table does not exist, `acquire()` returns true and nothing
 * is protected.
 *
 * Usage:
 * ```php
 * if ($locks->acquire('order:' . $orderId)) {
 *     $this->processOrder($orderId);
 * }
 * ```
 *
 * @see \App\UI\Cron\System\Presenters\SchedulerPresenter
 */
final class LockRegistry
{
    /** Lock states as stored in `cms_system_locks.state`. */
    public const
        StateFree = 0,
        StateHeld = 1;

    /** @var array<string, int> acquired lock names mapped to their row ids */
    private array $held = [];

    public function __construct(
        private readonly Connection $connection,
        private readonly Clock $clock,
    ) {
    }

    /**
     * Acquires the named lock and returns whether the caller now holds it.
     *
     * The call does not wait: a lock held by another request returns false immediately.
     *
     * @param string $name lock name, unique within the application (`order:<id>`)
     * @param int $ttlSeconds how long the lock survives a crashed request, in seconds
     * @return bool true also when the lock table does not exist (fail-open)
     * @throws LockException when the lock table exists but cannot be written
     */
    public function acquire(string $name, int $ttlSeconds = 60): bool
    {
        // Expired rows go first, otherwise a crashed request would keep its lock until
        // someone deletes the row by hand.
        $this->purgeExpired();

        if (!$this->tableExists()) {
            return true;
        }

        $expiresAt = DateTime::from($this->clock->now())->modifyClone('+' . $ttlSeconds . ' seconds');
        try {
            $this->connection->insert('cms_system_locks', [
                'name' => $name,
                'state' => self::StateHeld,
                'expires_at' => $expiresAt->format('Y-m-d H:i:s'),
            ]);
        } catch (\Doctrine\DBAL\Exception\UniqueConstraintViolationException) {
            return false;
        } catch (\Doctrine\DBAL\Exception $e) {
            throw new LockException('Cannot write lock "' . $name . '".', 0, $e);
        }

        $this->held[$name] = (int) $this->connection->lastInsertId();
        return true;
    }

    /**
     * Releases the named locks; names that are not held by this request are ignored.
     *
     * @param list<string> $names
     */
    public function release(array $names): void
    {
        foreach ($names as $name) {
            if (!isset($this->held[$name])) {
                continue;
            }
            $this->connection->delete('cms_system_locks', ['id' => $this->held[$name]]);
            unset($this->held[$name]);
        }
    }

    public function isHeld(string $name): bool
    {
        return isset($this->held[$name]);
    }

    /**
     * @deprecated use isHeld()
     */
    public function has(string $name): bool
    {
        return $this->isHeld($name);
    }

    private function purgeExpired(): void
    {
        if (!$this->tableExists()) {
            return;
        }
        $this->connection->executeStatement(
            'DELETE FROM cms_system_locks WHERE expires_at < ?',
            [$this->clock->now()->format('Y-m-d H:i:s')],
        );
    }

    private function tableExists(): bool
    {
        return $this->connection->createSchemaManager()->tablesExist(['cms_system_locks']);
    }
}

Anatomie docbloku metody#

/**
 * Acquires the named lock and returns whether the caller now holds it.
 *
 * The call does not wait: a lock held by another request returns false immediately.
 *
 * @param string $name lock name, unique within the application (`order:<id>`)
 * @param int $ttlSeconds how long the lock survives a crashed request, in seconds
 * @return bool true also when the lock table does not exist (fail-open)
 * @throws LockException when the lock table exists but cannot be written
 */
public function acquire(string $name, int $ttlSeconds = 60): bool
  1. Shrnutí: jedna věta, sloveso ve třetí osobě, říká i co metoda vrací.
  2. Popis (volitelný, do 5 řádků): předpoklady, vedlejší účinky, chování při selhání.
  3. Tagy v pořadí @param → @return → @throws → @see → @internal → @deprecated.

Nejmenší možný tvar je samotné shrnutí:

/**
 * Returns the zone by its id, or null when it does not exist.
 */
public function find(int $id): ?Zone

A tohle je přebytečný komentář — typ i jméno už signatura má, tag nic nepřidává:

/** @param int $id ID */           // smazat
public function find(int $id): ?Zone

Parametry a návratové hodnoty#

Signatura je zdroj pravdy o typech. Tag se píše, když přidává význam nebo užší typ, a vždy ve tvaru typ, název, popis. Tag, který by nesl jen nativní typ, se nepíše.

Kdy popsat parametr Příklad
jednotky @param int $ttlSeconds … in seconds
význam speciální hodnoty @param int $limit 0 means no limit
tvar hodnoty @param string $name lock name (order:)
užší typ pole @param list<int> $ids, @param array<string, mixed> $options

Shrnutí pojmenuje, co metoda vrací. @return se přidá, když je potřeba užší typ nebo když význam variant není zřejmý:

Návratový typ Co musí být jasné
bool co znamená true a co false
?Entity kdy přijde null — neexistuje, nebo není dostupná uživateli
array tvar: list<Zone>, array<string, int>, array{id: int, name: string}; nikdy holé array
int co číslo je: počet, id, pozice
void, static nic; @return void se nepíše
Result které kódy může nést a kdy (místo výjimek)

Totéž platí pro vlastnosti: @var jen s tvarem pole nebo generikem, a pak i s popisem (@var array<string, int> number of items grouped by category key).

Výjimky a vedlejší účinky#

  • @throws Výjimka when <podmínka> u výjimek, které metoda záměrně vyhazuje nebo propaguje jako součást kontraktu, v každé vrstvě a viditelnosti. Technické výjimky závislostí, které metoda neřeší, se neuvádějí.
  • Když metoda místo výjimky vrací Result s kódy, popis uvádí kódy.
  • Dokumentace odpovídá chování: metoda vracející null netvrdí, že vyhazuje.
  • Vedlejší účinky patří do popisu, když by jejich neznalost vedla ke špatnému použití: zápis nebo mazání dat, změna stavu entity, invalidace cache, síťové nebo interní API volání, kontrola oprávnění, chování při výpadku závislé služby (fail-open, fail-closed).

Zastaralé API a odkazy#

  • @deprecated use Y::method() — náhrada musí existovat; bez „since“ a bez data.
  • Odkaz na třídu nebo metodu: {@see Class::method()}.
  • Dlouhý výklad má v docbloku jen @see https://manual.coolsoft.cz/…, nejvýš jeden a jen na existující stránku.

Co ukázky předvádějí#

Prvek Pravidlo
Před namespace nic — žádná licence, autor, název souboru ani datum
Docblok třídy těsně nad class; první řádek = jedna věta s tečkou, co třída je; pak popis běžně do 10 řádků: kontrakt, invarianty, pasti. Delší smí být, jen když je každý řádek kontrakt; výklad (proč to vzniklo, srovnání, plány) jde do manuálu a docblok na něj odkáže
Presenter shrnutí nese i adresu: … (/admin/store/zone)
Usage: blok kódu jen tam, kde tvar použití není zřejmý ze signatury (atributy, komponenty, služby) — místo tagu @example
use XxxTrait; trailing komentář = seznam členů, které trait dodává, metody s ()
Vlastnost jednořádkový /** … */; @var jen s tvarem pole nebo generikem, pak i s popisem
Konstanty jeden popisek nad skupinou, ne nad každou
Řádkový // nad kódem, 1–3 řádky, celá věta s tečkou; vysvětluje proč a s jakým následkem
Oddělovače sekcí třířádkový pruh // ===, název = role metod (Actions, Components, Helpers); jen v delších souborech
Identifikátory v backtickách, metody s ()
Pořadí tagů třídy @template → @extends / @implements → @use → @property / @method → @see → @internal → @deprecated

Značky#

Značka Kdy Příklad
IMPORTANT: porušení = chyba, bezpečnostní díra, ztráta dat; vždy s následkem // IMPORTANT: server-side gate. Without it the action is reachable by URL.
NOTE: nezřejmé, ale neškodné — záměrná odchylka, překvapivé chování // NOTE: the warehouse column is hidden on purpose …
TODO: známá mezera a co udělat; bez data, jména a čísla úkolu // TODO: move the filter into ZoneManager::findByWarehouse() …

Značka stojí na začátku komentáře, velkými písmeny s dvojtečkou, nejvýš jedna. Jiné značky (FIXME, HACK, XXX), emoji a vykřičníky se nepoužívají.

Co do komentáře nepatří#

  • historie: „dřív“, „původně“, data, čísla nálezů a úkolů, jména, odkazy na interní poznámky — pravidlo, které z historie plyne, zůstává,
  • opakování kódu (// save entity nad $manager->save()) a tagy opakující signaturu,
  • zakomentovaný kód,
  • nadpisy, tabulky, dlouhé výklady „proč to existuje“ — ty patří do manuálu,
  • zdůraznění velkými písmeny (NEVER, ONLY); důraz nese značka nebo věta,
  • čeština, ani v uvozovkách; text z UI se cituje překladovým klíčem.

Co se kontroluje#

Kontrola Čím
změnily se jen komentáře — tokeny kódu jsou beze změny tests/Manual/Comments/comment_guard.php
česká diakritika a emoji v komentářích comment_guard.php --strict
typ v tagu odpovídá signatuře, syntaxe generik, neznámé třídy v tazích PHPStan
zakázané značky, nadpisy a tabulky v docbloku, @example, @author, komentář před namespace, zdůraznění verzálkami comment_guard.php --strict (rozšíření v přípravě)
tagy opakující nativní typ Rector, hromadně po oblasti
nerozpoznané {@see} a tagy, které generátor neumí phpdoc --validate po oblasti

Nehlídá se nepřítomnost docbloku u getterů, konstruktorů a action*: není povinný, není zakázaný. Že text odpovídá chování, ověří jen revize.

Kam sáhnout#

Chci Kde
závazné znění a slovník termínů skills/code-standard/cs-comments/SKILL.md
vzorové soubory z této stránky skills/code-standard/cs-comments/examples/
kostra souboru a pojmenování metod Konvence kódu

Navazující kapitoly: Konvence kódu · Pětivrstvý model · Testy a nástroje

Nastavení cookies