phpFashion

Na navigaci | Klávesové zkratky

Rubrika PHP

Chrome a nekonečné přesměrování

Chrome 44 (beta) odesílá nově hlavičku HTTPS: 1, která může způsobovat problémy.

Na některých hostinzích (z těch co používám třeba WebSupport už to opravili) si pak PHP myslí, že požadavek je pod šifrovaným spojením HTTPS. Tj. proměnná $_SERVER['HTTPS'] === 'on'.

U aplikací v Nette, které neběží pod https, to pak způsobí nekonečný redirect. Aplikace si prostě myslí, že k ní přistupujete přes URL https://example.com a přesměrovává na http://example.com.

Můžete to vyzkoušet z příkazové řádky pomocí:

curl -I --header "HTTPS: 1" http://example.com`

Že je hlavička HTTPS: 1 problematická se už ví, takže je možné, že se změní a do Chrome nedostane. Každopádně jako rychlý workaround, aby nedocházelo ke smyčce přesměrování v betaverzi Chrome, je přidat na začátek bootstrap.php:

unset($_SERVER['HTTPS']);

Zároveň je dobré si uvědomit, že na některých hostinzích lze detekci šifrovaného spojení velmi snadno ošálit.

Doplnění: Chrome 45 už hlavičku HTTPS: 1 neodesílá.


Jak na převod array() na [] v Gitu

Jak jsem v projektech, které používají PHP 5.4 a vyšší, převáděl starou syntaxi pro zápis polí array() na novou [].

Samotný převod PHP souborů je úplně jednoduchý. Stačí použít PHP 5.4 Short Arrays Converter a v repozitáři zavolat:

php /php54-arrays/convert.php .

Nástroj zamění syntax ve všech souborech *.php a *.phpt v aktuálním adresáři a také ve všech podadresářích. Změněné soubory jsem pak komitnul (příklad).

Oříšek je ale rebasování dalších větví na takto změněný master.

Nakonec jsem na to šel přes filtry. Ale plně zautomatizovat se mi to nepovedlo.

Nejprve je vhodné všechny větve rebasovat na master těsně před samotnou změnou syntaxe.

Poté jsem si vytvořil filtr nazvaný phparray, který bude on-the-fly překládat v PHP souborech [] na array() při checkoutu a obráceně při komitování. Tedy aby slučování probíhalo při použití staré syntaxe, ale komitnulo se s novou.

Filtr se vytvoří v souboru .git/config, v mém případě to vypadalo takto:

[filter "phparray"]
    clean = c:/php/php.exe w:/php54-arrays/convert.php
    smudge = c:/php/php.exe w:/php54-arrays/convert.php -r
    required

Aby se filtr při slučování používal (ale i při každém checkoutu, cherry-picku atd), je nutné doplnit do .git/config ještě následující:

[merge]
    renormalize = true

Filtr se bude aplikovat na soubory *.php a *.phpt, což se definuje v souboru .git/info/attributes (nepoužívejte .gitattributes, protože jde o dočasnou záležitost a nechceme ji komitovat):

*.php filter=phparray
*.phpt filter=phparray

Teď by ve větvi mělo fungovat git rebase master. Bez konfliktů, které by bylo nutné ručně řešit. Jenže ouha, konflikty se mi vytvářely (na Windows; je možné, že na Linuxu to půjde) a když jsem je chtěl řešit v TortoiseGit, objevovala se hláška, že na souboru .git/index.lock je zámek atd. Zkoušel jsem experimentovat s dalšími nastaveními pro merge, ale bez výsledku.

Zkusil jsem dělat rebase ručně: což znamená nejprve větev resetnout na master (git reset master --hard) a pak jednotlivé komity přidávat pomocí git cherry-pick <hash>. Nedělal jsem to z příkazové řádky, ale pomocí TortoiseGit. I nadále mi hlásil, že některé soubory neumí automaticky sloučit a je vyžadován ruční zásah, ale vždy stačilo soubor rozkliknout do TortoiseGitMerge a rovnou stisknout Mark as resolved.

Bylo to otravné, ale fungovalo to a postupně jsem „rebasoval“ všechny větve. Poté jsem filtr z .git/config a .git/info/attributes smazal.

Proč to nešlo úplně automaticky, nemám páru, nicméně bylo snazší hodinu klikat, než dva dny studovat Git.


PHP 5.4 Short Arrays Converter

Command-line script to convert between array() and PHP 5.4's short syntax []. It uses native PHP tokenizer, so conversion is safe. The script was successfully tested against thousands of PHP files.

Download from GitHub

To convert all *.php and *.phpt files in whole directory recursively or to convert a single file use:

php convert.php <directory | file>

To convert source code from STDIN and print the output to STDOUT use:

php convert.php < input.php > output.php

To convert short syntax [] to older long syntax array() use option --reverse:

php convert.php --reverse [<directory | file>]

Composer: jak na různé varianty instalace

Composer, nejdůležitější nástroj pro PHP vývojáře, umožňuje 3 způsoby, jak instalovat balíčky:

  • lokální composer require vendor/name
  • globální composer global require vendor/name
  • jako projekt composer create-project vendor/name

Lokálně

Lokální instalace se používá nejčastěji. Mám projekt, ve kterém chci třeba použít Tracy, tak v kořenovém adresáři projektu zadám:

composer require tracy/tracy

a Composer zaktualizuje (nebo vytvoří) soubor composer.json a stáhne Tracy do podsložky vendor. Zároveň vygeneruje autoloader, takže v kódu jej stačí inkludovat a můžu rovnou Tracy použít:

require __DIR__ . '/vendor/autoload.php';
Tracy\Debugger::enable();

Nástroje

Diametrálně odlišná situace nastává tehdy, pokud místo knihovny, jejíž třídy ve svém projektu používám, instaluji nástroj, který jen spouštím z příkazové řádky.

Příkladem může být třeba ApiGen pro generování přehledných API dokumentací. V takovém případě se použije třetí způsob:

composer create-project apigen/apigen

Composer vytvoří novou složku (a tedy i nový projekt) apigen a do ní stáhne celý nástroj a nainstaluje jeho závislosti.

Bude mít vlastní composer.json a vlastní podsložku vendor.

Tímto způsobem se instaluje i třeba Nette Sandbox nebo CodeChecker. Nikoliv však testovací nástroje jako je Nette Tester nebo PHPUnit, protože jejich třídy naopak v testech používáme, voláme Tester\Assert::same() nebo dědíme od PHPUnit_Framework_TestCase.

Bohužel Composer umožňuje instalovat nástroje jako je ApiGen i pomocí composer require a nevypíše ani žádné varování.

Což je totéž, jako když donutíte dva vývojáře, kteří se ani neznají a kteří pracují na úplně jiném projektu, aby sdíleli společnou složku vendor. Na to se dá říci:

  • Proboha proč by to měli dělat?
  • Vždyť to přece nemůže fungovat!

Ano, není žádný rozumný důvod to dělat, nic to nepřinese, naopak to přestane fungovat v momentě, kdy dojde ke kolizi používaných knihoven. Je to jen otázka času, stavění domečku z karet, který se dřív nebo později sesype. Jeden projekt bude vyžadovat knihovnu XY ve verzi 1.0, druhý ve verzi 2.0 a v tu chvíli to přestane fungovat.

Globálně

Rozdíl mezi variantou 1) a 2), tj. mezi composer require a composer global require, je pak v tom, že nepůjde o dva cizí vývojáře, ale o deset cizích vývojářů a deset nesouvisejících projektů. Tedy je to nesmysl na druhou.

Totiž composer global je špatné řešení úplně vždy, neexistuje use case, kdy by bylo vhodné jej použít. Byl jsem zvědavý, jak se mohlo něco takového dostat do Composeru, a vypadá to, nepletu-li se, že tento nesmysl přidal můj oblíbený troll :-)

(Jako ukázku si můžete globálně nainstalovat dg/composer-global-is-useless. Poté už nepůjde globálně nainstalovat ani PHPUnit.).

Rekapitulace

  • composer require vendor/name pokud chcete používat třídy knihovny
  • composer global require vendor/name nikdy!
  • composer create-project vendor/name pro nástroje volané jen z příkazové řádky

Poznámka: npm používá odlišnou filosofii danou možnostmi JavaScriptu a každou knihovnu instaluje jako „samostatný projekt“, s vlastním adresářem vendor (resp. node_modules). Ke konfliktu verzí tak dojít nemůže. V případě npm naopak platí, že globální instalace nástrojů, jako je například LESS CSS, jsou velmi užitečná a příjemná věc.


Jak na GitHub a pull request z příkazové řádky

Tuhle jsem zveřejnil skript na cherry-pickování přímo z GitHubu, který dodnes používám, ale bylo otravné tím stahovat celé pull requesty, pokud obsahovaly víc komitů. Takže jsem ho naučil stahovat je na jeden zátah. Opět stačí jako argument uvést URL:

php pullpick.php https://github.com/nette/tracy/pull/58

Oproti cherry-picku je potřeba navíc zjistit zdrojový repozitář a větev, k čemuž použijeme GitHub API. Skript vypadá takto:

<?php
$url = @$_SERVER['argv'][1];

if (!preg_match('#github.com/([^/]+)/([^/]+)/pull/(\w+)#', $url, $m)) {
    die('Invalid URL');
}

list(, $name, $repo, $pull) = $m;

$context = stream_context_create(array('http' => array('user_agent' => 'Me')));
$info = file_get_contents("https://api.github.com/repos/$name/$repo/pulls/$pull", false, $context);
$info = json_decode($info);
if (!isset($info->head->repo->clone_url, $info->head->ref)) {
    die('Missing repo info.');
}
passthru("git checkout -b pull-$pull master");
passthru("git pull --no-tags {$info->head->repo->clone_url} {$info->head->ref}");

Pull request se stáhne do nové větve s názvem jako pull-123.

Mám i skript na vytvoření nového pull requestu. Spustíte jej ve větvi, ze které chcete PR vytvořit, bez parametrů. On větev pushne do vašeho forku a poté otevře prohlížeč s formulářem pro vytvoření pull requestu:

<?php
$remote = 'dg'; // tady dejte název 'remote' vedoucí k forku na GitHubu

exec('git remote -v', $remotes);

$repo = null;
foreach ($remotes as $rem) {
    if (preg_match('#^' . preg_quote($remote) . '\tgit@github.com:(.+)\.git \(#', $rem, $m)) {
        $repo = $m[1];
        break;
    }
}

if (!$repo) {
    die('Not Github repo');
}

exec('git rev-parse --abbrev-ref HEAD', $branch);
$branch = $branch[0];
if (!$branch) {
    die('Unable to retrieve branch name');
}

echo "Pushing to $repo & $branch\n";
exec("git push --set-upstream $remote $branch");

$url = "https://github.com/$repo/compare/$branch?expand=1";
exec('start "" ' . $url); // tohle otevře prohlížeč pod Windows. Pro jiné OS si upravte.

Proč Nette nedodržuje standardy PHP-FIG / PSR?

„Proč Nette musí jít za každou cenu proti proudu a odmítá respektovat standardy PHP-FIG?“

Tohle je sugestivní otázka, často kladená v souvislosti s tím, proč Nette odsazuje tabulátory, a nikoliv mezerami. Pojďme to rozebrat postupně.

V první řadě, PHP Framework Interoperability Group (PHP-FIG) netvoří obecné standardy, jde o soubor pravidel, která si tvoří určitá viditelná skupina vývojářů sama pro sebe. Ve FAQ se uvádí, že původní název „PHP Standards Group“ byl změněn právě proto, že byl zavádějící. Doslova říkají „pokud chcete dodržovat naše standardy, prosím, ale není to našim záměrem.

Jelikož naprostá většina pravidel coding standardů PHP-FIG se shoduje se standardem Nette, dá se říci, že Nette je, až na drobné výjimky, dodržuje také.

Dokument PSR-0 Autoloading Standard specifikuje, že každá třída musí být uložena v souboru, jehož cesta přesně odpovídá názvu třídy. Jelikož Nette disponuje pokročilejším autoloadingem (a výkonnějším, viz níže) než mají ostatní frameworky, nemusí se tohoto pravidla držet z technické nutnosti, nicméně jde o přehledný způsob organizace souborů, proto jej také používá. Jedinou odchylkou jsou definice výjimek, které je někdy praktičtější umístit do jednoho souboru – jsou tak pěkně pohromadě a např. adresář Utils zůstává výrazně přehlednější.

PSR-0 odchylky netoleruje, právě kvůli technickým omezením, které se Nette netýkají (a v dnešní době Composeru se netýkají vlastně žádné knihovny). Volání po rigidním dodržení PSR-0 by bylo zbytečným krůčkem zpět.

Dokument PSR-1 Basic Coding Standard definuje základní pravidla pro obsah PHP souboru, která se plně slučují s coding standardem Nette.

Dokument PSR-2 Coding Style Guide je obsáhlou definicí přesných pravidla pro zápis kódu. Takřka kompletně se slučuje se standardem Nette, krom těchto bodů:

  • zatímco PSR vyžaduje psát true, false, null malými písmeny, Nette (mimochodem v souladu s dokumentací PHP) používá velká písmena Nette již používá malá písmena
  • Nette odsazuje pomocí tabulátorů, PSR-2 vyžaduje mezery.

Dokument PSR-3 Logger Interface definuje rozhraní Psr\Log\LoggerInterface, čímž opouští roli kodifikátora pravidel psaní kódu. Dle mého je rozhraní definováno špatně a řeší problém, který neexistuje. Ale tohle téma nechci probírat, ani v komentářích. Můžete si přečíst třeba názor ircmaxella + pokračování. Chystaný PSR-11 Container Interface je pak příkladem naprostého nepochopení Dependency Injection.

Dokument PSR-4 Autoloader určuje, jak má být implementovaný autoloader pro PSR-0. Jak jsem zmiňoval, tohle téma je pro Nette a Composer passé. Navíc se v praxi ukázalo, že jde o řešení s negativním dopadem na výkon, pročež Composer nabízí volbu --optimize, která převede PSR-0 na classmap, kterou právě používá Nette RobotLoader.

Když už je shoda mezi PSR a standardem Nette tak velká, proč nezměnit těch pár drobností a nebýt 100% vyhovující? Je v tom tvrdohlavost? Nebo zmíněná snaha jít proti proudu?

Ani jedno. Ohledně znaku pro odsazování jsem se po celoživotním používání mezer nechal netvrdohlavě přesvědčit a přešel na tabulátory. Stalo se tak rok před vznikem PHP-FIG, tedy onoho proudu, proti kterému bych měl jít.

Zajímavé je, že dokonce i řada samotných členů PHP-FIG používají tabulátory. Jeden ze zakladatelů Paul M. Jones mi k tomu napsal: „Yes, the vote was roughly 2:1 in favor of spaces. The tab folk are as a result not PSR-2 compliant.“

Jednoduše prostě neznám důvod, proč takhle zasáhnout do kódu a leccos tím zkomplikovat. Nemáme jen jeden jediný standard, tabulátory používá třetina všech projektů. Navíc otázka tabulátoru vs mezery je naprosto malicherná a nemá naprosto žádný vliv na interoperabilitu knihoven.

Jediné podstatné je být konzistentní a v celém kódu dodržovat stejná pravidla. Nesmí být poznat, že kód psalo více lidí. Což Nette striktně odjakživa respektuje.


Jak v PHP detekovat chybu? No těžko…

Do žebříčku 5 největších zrůdností jazyka PHP rozhodně patří nemožnost zjistit, zda volání nativní funkce skončilo úspěchem, nebo chybou. Ano, čtete správně. Zavoláte funkci a nevíte, zda došlo k chybě a k jaké.

Teď si možná klepete na čelo a říkáte: selhání přece poznám podle návratové hodnoty, ne? Hmmm…

Návratová hodnota

Nativní (nebo interní) funkce obvykle vracejí v případě neúspěchu false. Jsou tu výjimky, například json_decode, která vrací null, pokud je vstup nevalidní nebo překročí limit zanoření. Což najdeme v dokumentaci, potud ok.

Tato funkce slouží k dekódování JSONu i jeho hodnot, tedy volání json_decode('null') také vrátí null, tentokrát ale jako korektní výsledek. Musíme tedy rozlišovat null jakožto správný výsledek a null jakožto chybu:

$res = json_decode($s);
if ($res === null && $s !== 'null') {
    // došlo k chybě
}

Je to hloupé, ale pámbů zaplať, že to vůbec lze. Existují totiž funkce, u kterých nelze z návratové hodnoty poznat, že k chybě došlo. Např. preg_grep nebo preg_split vrací částečný výsledek, tedy pole, a nepoznáte vůbec nic (více v Zrádné regulární výrazy).

json_last_error & spol.

Funkce informující o poslední chybě v určitém rozšíření PHP. Bohužel bývají mnohdy nespolehlivé a je obtížné zjistit, co to vlastně ta poslední chyba je.

Například json_decode('') neresetuje příznak poslední chyby, takže json_last_error vrací výsledek nikoliv pro poslední, ale pro nějaké předchozí volání json_decode (viz How to encode and decode JSON in PHP?). Obdobně ani preg_match('neplatývýraz', $s) neresetuje preg_last_error. Pro některé chyby nemají tyto funkce kód, takže je vůbec nevrací, atd.

error_get_last

Obecná funkce vracející poslední chybu. Bohužel je nesmírně komplikované zjistit, zda se chyba týkala vámi volané funkce. Onu poslední chybu totiž mohla vygenerovat úplně jiná funkce.

První možností je přihlížet ke error_get_last() jen ve chvíli, kdy návratová hodnota značí chybu. Bohužel třeba funkce mail() umí vygenerovat chybu, i když vrátí true. Nebo naopak preg_replace v případě neúspěchu nemusí chybu generovat vůbec.

Druhou možností je před voláním naší funkce „poslední chybu“ vyresetovat:

@trigger_error('', E_USER_NOTICE); // reset

$file = fopen($path, 'r');

if (error_get_last()['message']) {
    // došlo k chybě
}

Kód je zdánlivě jasný, chyba může vzniknout pouze při volání funkce fopen(). Ale není tomu tak. Pokud je $path objekt, bude převeden na řetězec metodou __toString. Pokud je to jeho poslední výskyt, bude volán i destruktor. Mohou se volat funkce URL wrapperu. Atd.

Tedy i zdánlivě nevinný řádek může vykonat spoustu PHP kódu, který může generovat jiné chyby, z nichž poslední pak vrátí error_get_last().

Musíme se proto ujistit, že k chybě došlo skutečně při volání fopen:

@trigger_error('', E_USER_NOTICE); // reset

$file = fopen($path, 'r');

$error = error_get_last();
if ($error['message'] && $error['file'] === __FILE__ && $error['line'] === __LINE__ - 3) {
    // došlo k chybě
}

Ona magická konstanta 3 je počet řádků mezi __LINE__ a voláním fopen. Prosím bez komentáře.

Tímto způsobem už chybu odhalíme (tedy pokud ji funkce emituje, což třeba zmíněné funkce pro práci s regulárními výrazy zpravidla nedělají), ale nejsme schopni ji potlačit, tedy zabránit tomu, aby se zalogovala apod. Použití například shut-up operátoru @ je problematické v tom, že zatají vše, tedy veškerý další PHP kód, který se v souvislosti s naší funkcí volá (viz zmíněné destruktory, wrappery atd.).

Vlastní error handler

Šíleným, ale zřejmě jediným možným způsobem, jak zjistit, zda určitá funkce vyhodila chybu s možností ji potlačit, je instalace vlastního chybového handleru pomocí set_error_handler. Jenže není sranda to udělat správně:

  • vlastní handler musíme také odstranit
  • musíme jej odstranit i v případě, že se vyhodí výjimka
  • musíme zachytávat skutečně jen chyby vzniklé v inkriminované funkci
  • a všechny ostatní předat původnímu handleru

Výsledek vypadá takto:

$prev = set_error_handler(function($severity, $message, $file, $line) use (& $prev) {
    if ($file === __FILE__ && $line === __LINE__ + 9) { // magická konstanta
        throw new Exception($message);
    } elseif ($prev) { // volej předchozí uživatelský handler
        return call_user_func_array($prev, func_get_args());
    }
    return false; // volej systémový handler
});

try {
    $file = fopen($path, 'r');  // o tuhle funkci nám jde
    restore_error_handler();
} catch (Exception $e) {
    restore_error_handler();
    throw $e;
}

Co je magická konstanta 9 už víte.

No a tak my v PHP žijem, no.


Jak komitovat se záruční smlouvou?

Už jsem odpověděl na spoustu pull requestů „Can you add tests?“ Ale ne proto, že bych byl testofil, nebo abych dotyčného buze prudil.

Pokud posíláte pull request, který opravuje nějakou chybu, tak pochopitelně musíte před odesláním vyzkoušet, jestli skutečně funguje. Kolikrát si člověk myslí, že něco snadno fixne a ejhle, rozbije to ještě víc. Nechci se opakovat, ale tím, že to vyzkoušíte, jste vyrobili test, tak ho jen přiložte.

(Bohužel někteří lidé svůj kód doopravdy nevyzkouší. Kdyby to šlo, dával bych měsíční bany za pull requesty vytvořené přímo ve webovém editoru Githubu.)

Ale to stále není ten nejhlavnější důvod: Test je jediná záruka, že vaše oprava bude fungovat v budoucnu.

Už mnohokrát se stalo, že někdo poslal pull request, který mi nebyl užitečný, ale upravoval funkcionalitu důležitou pro něj. Zejména pokud to byl někdo, koho znám, a vím, že je dobrý programátor, tak jsem to mergnul. Pochopil jsem, k čemu to chce, nevadilo to ničemu jinému, tak jsem PR přijal a v tu chvíli vypustil z hlavy.

Pokud svůj pull request doplnil testem, tak jeho kód dodnes funguje a bude fungovat i nadále.

Pokud ho testem nedoplnil, tak se klidně může stát, že mu to nějaká další úprava rozbije. Ne schválně, prostě se to stane. Nebo už se to stalo. A nemá smysl láteřit, jaký jsem vůl, že jsem mu už potřetí rozbil jeho kód, ačkoliv jsem před 3 lety přijal jeho pull request, si to snad musím pamatovat né, takže mu to snad dělám naschvál… Nedělám. Nikdo si nepamatujeme, co jsme měli před třemi lety na svačinu.

Pokud vám na nějaké funkcionalitě záleží, přiložte k ní test. Pokud vám na ni nezáleží, vůbec ji neposílejte.


Alpha-beta-RC is so 20th century

Přestanu používat při vývoji označení testovacích fází alpha/beta/RC a nahradím ho jedním slovem.

Existuje řada způsobů, jak software verzovat. U knihoven se často hovoří o sémantickém verzování, kdy verzi reprezentuje trojice čísel major.minor.patch, a hlavní číslo major musí být zvýšeno pokaždé, když se v kódu objeví zpětně nekompatibilní změna. Což zní na první pohled rozumně, ale z mnoha důvodů to takřka nikdo 100% nedodržuje.

Taktéž existují různé způsoby, jak označovat jednotlivé fáze vývoje, z nichž nejznámější je asi slovní označení alpha/beta/RC. V beta fázi by měl být produkt kompletní co se vlastností týče a dále by se měly opravovat chyby, ladit kompatibilita atd. K čemuž se opět přistupuje různě, asi největším extrémem jsou věčné bety, které zpopularizoval Google. Opačným případem je prohlížeč Chrome, který přišel se zrychleným vydáváním verzí a přelévá mezi kanály dev/beta/stable.

Jak vidno, přístupy se u jednotlivých projektů značně liší. Záleží na mnoha faktorech, velikostí týmu počínaje, marketingem konče. Pro mě je prioritou, aby systém vedl k:

  • vydávání odladěných major & minor verzí
  • při maximální snaze zachovat zpětnou kompatibilitu
  • a pokud možno v pravidelných intervalech, včetně přísunu novinek

Což se lehko řekne, mnohem těžší je se k tomu dobrat, navíc aby to bylo v lidských silách.

Flow, které mi celkem vyhovuje a uvedené priority vesměs plní, vypadá následovně:

  1. vývoj probíhá neprve ve větvích (buď veřejně v podobě pull requestů nebo lokálně)
  2. mergnutím do masteru (což je vlastně permanentní alpha-verze) začíná testování. Přičemž žádný commit nesmí rozbít testy, ale může narušit zpětnou kompatibilitu (viz dále)
  3. jednou za půl roku až rok bych rád vydal větší verzi:
    • vytvořím release-větev s názvem jako v3.2
    • musím rozhodnout, které věci vynechat (revert) a které přidat (merge pull requestů a lokálních větví)
    • především však udělat co nejvíc pro zachování kompatibility rozbité v bodě 2)
    • v krátkých intervalech vydávat testovací verze reagující na chyby a připomínky (což bývá zápřah)
  4. a pak po dlouhou dobu udržovat verzi čerstvou
    • cherry-pickováním oprav z masteru (nebo naopak, ale výjimečně někdo připraví pull request oproti release-větvi)
    • vyhýbat se BC breakům (byť je někdy značně ošidné rozlišit, co BC break je)
    • vydávat patch verze (tj. setinkové) v krátkých intervalech

Někomu to připadá jako úplně normální postup, jiní zase brblají, takže musím zmínit, na jaká v reálu narážím úskalí.

S každými novinkami přichází (byť třeba jen hypotetické) BC breaky. To je prostě realita. Přičemž zachovávání kompatibility beru jako velmi důležitou věc a stojí mě tak 70 % času investovaného do vývoje. Je taky nejčastějším důvodem, proč otálím s přidáním nových věcí. V případě Texy to vedlo v podstatě až k ukončení vývoje.

Existuje dost frameworků, které přežily tlustou čáru mezi dvěma major verzemi. Já ji ale dělat nechci. Nechci ani utopit vývoj. Kloním se proto k postupným zdokumentovaným BC breakům u větších verzí. Navádím uživatele co změnit pomocí E_USER_DEPRECATED hlášek. Nejvýhodnější situaci tak mají ti, kteří průběžně updatují.

Nicméně dle sémantického verzování bych proto měl každého půl roku až rok vydat novou major verzi, což se mi nejeví z řady důvodu praktické. Zejména to vyvolává dojem, že přechod bude velmi náročný a uživatelé začnou setrvávat u starých verzí. Což nechci. Proto raději zvedám jen minor verzi (tj. desetinkovou).

Podle sémantického verzování se novinky mohou objevovat jen v major & minor verzích. Vydávání těchto verzí v intervalech kratších než půl roku vytváří dojem, že vývoj uhání příliš rychle, a uživatelé opět přestávají aktualizovat. Ovšem přijít s užitečnou novinkou a odkládat její vydání i půl roku a více, než bude čas na další větší verzi, nepřináší užitek nikomu, a uživatele toužícího po novince tlačí k používání masteru. Což opět nechci. Takže občas zařadím novinky i do bodu 4). Nepříjemné je, že si kvůli tomu vyslechnete dost kritiky, ještě nepříjemnější je, že po letech jejího permanentního poslouchání už nemáte jakoukoliv schopnost rozlišovat kritiku oprávněnou od kritiky české.

Pokud vydám alpha verzi a poprosím o testování, zhostí se toho naprosté minimum lidí. O moc lépe na tom nejsou ani první beta-verze. Teprve až začnu vydávat RC, na fóru to ožije. Což je mnohokrát ověřený fakt. Nepříjemné je, že i když následně vydáte sebestabilnější verzi, bude vám neustále někdo kazit náladu vysvětlováním, jak to babráte a nerozumíte dělení na alpha/beta/RC. Z jeho pohledu jakoby oprávněně, tudíž jsem se rozhodl, že nebudu nadále rozlišovat tyto fáze, protože to nikomu nic nepřináší, cíli udělat co nejlepší stable podřizuji vše nehledě na fázi, a bude stačit vydávat jen číslované -testing verze (v podstatě beta-verze).

Dává smysl mi ukončit tuto testovací fázi a vydat stable ve chvíli, kdy sám jsem spokojený a reportování chyb poklesne na minimum. Samozřejmě mohl bych pár měsíců čekat, ale realita je taková, že aktivita se nastartuje teprve až vydáním stable.

Ještě jednou to shrnu:

  • master je alpha-verze, do které se dostanou jen nadějné novinky + bugfixy, a bez záruky zpětné kompatibility se tu testují
  • jednou za čas vytvořím release-větev, která po sérii -testing verzí řešících mj. kompatibilitu bude završena rychlým vydáním stable
  • mezi masterem a release-větví cherry-pickuji opravy, občas menší novinky, a vydávám setinkové verze, ideálně s jedním RC

Docela jsem zvědavý, co v případě Nette udělá s uvedeným flow rozdělení na malé projekty. Teoreticky bych mohl najet na rychlé verzování u částí, které se budou vyvíjet, a uživatel by si pomocí Composeru poskládal takové Nette, jaké by chtěl. Zatím ale spíš narážím na nejrůznější překážky a vydávání nových verzí je teď mnohem větší dřina.


Nevěřím statistikám, které si sám nezfalšuji

Václav Novotný připravil infografiku porovnávající aktivitu vývojářů v Nette a Symfony. Rád a zvědavě se podívám, leč bez vysvětlení metriky umí být čísla krutě zrádná. S nadsázkou: při určitém workflow a naivním měření mohu ve statistikách vyjít jako autor 100 % kódu, aniž bych naprogramoval jedinou řádku.

I při přímočarých workflow je poměřování množství komitů zákeřné. Není komit jako komit. Pokud přidáte pětici důležitých komitů a zároveň deset lidí vám opraví překlepy v komentářích, jste co do počtu komitů autorem třetiny kódu. Což ale není pravda, jste autorem celého kódu, opravy překlepů se za autorství (jak ho obvykle vnímáme) nepovažují.

V GITu dále věc komplikují „merge-commits“. Pokud někdo připraví zajímavý komit a vy ho schválíte (tedy vznikne ještě merge-commit), jste autorem poloviny komitů. Ale jaký je vlastně skutečný podíl? Obvykle nulový, schválení je otázka jednoho kliknutí v GitHubu, byť někdy diskusí strávíte víc času, než kdybyste si kód napsal sám, ale neuděláte to, protože potřebujete vývojáře vychovávat.

Proto místo počtu komitů je vhodnější analyzovat jejich obsah. Nejjednodušší je brát v úvahu počet změněných řádek. Ale i to může být zavádějící: pokud vytvoříte 100 řádkovou třídu a někdo jiný soubor s ní jen přejmenuje (či rozdělí na dva), „změnil“ vlastně 200 řádků a opět jste autorem třetiny.

Pokud týden ladíte u sebe několik komitů a až potom je pošlete do repozitáře, jste v počtu změněných řádek v nevýhodě oproti tomu, kdo je pošle hned a teprve poté dolaďuje následujícími komity. Nebylo by tedy od věci analyzovat třeba až souhrny za celý den. Je třeba odfiltrovat i údržbové komity, zejména ty, které mění u všech souborů letopočet nebo verzi v hlavičce.

Do toho přicházejí ještě situace, kdy se automatizovaně kopírují komity z jedné větve do jiné, nebo do jiného repozitáře. Což de facto znemožňuje dělat jakékoliv globální statistiky.

Analýza jednoho projektu je věda, natož ta srovnávací. Docela mi to připomíná skvělý analytický kvíz od Honzy Tichého.


Související: Jak se počítá „Hall of fame“ na nette.org


phpFashion © 2004, 2018 David Grudl | o blogu

Ukázky zdrojových kódů smíte používat s uvedením autora a URL tohoto webu bez dalších omezení.