phpFashion

Na navigaci | Klávesové zkratky

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.

Pět důvodů upgradovat na Nette 2.2.3

Mám skvělý pocit z právě vydané verze Nette 2.2.3, protože se tam podařilo vychytat řadu drobností, počínaje chytřejším rozpoznání chyb u nativních funkcí, přes výstižnější chybové hlášky DI kontejneru, až po různé novinky (viz release notes). Už jsem ji nasadil na všechny své weby a běží výborně.

Rád bych vypíchl 3 užitečné novinky. První se týká Latte a jde o funkci invokeFilter(), kterou můžete volat filtr i mimo šablonu:

$latte = new Latte\Engine;
$latte->addFilter('upper', 'strtoupper');
$upper = $latte->invokeFilter('upper', array('abc')));
// obdoba {'abc'|upper} v šabloně

Druhá novinka se týká Tracy. Ta nyní dokáže logovat chyby jako je E_NOTICE v plné náloži (tj. s HTML souborem), jako když loguje výjimky. Které chyby má takto logovat nastavíte do proměnné $logSeverity:

Tracy\Debugger::$logSeverity = E_NOTICE | E_WARNING;

Třetí novinka souvisí s bezpečností. Třída Configurator má metodu setDebugMode(), pomocí které určujete, zda aplikace poběží v produkčním nebo vývojářském režimu. Raději jí nikdy nepředávejte argument TRUE, může se pak snadno stát, že to deploynete na ostrý server a máte hned bezpečnostní kráter. Správné je jako argument předat IP adresy, pro které chcete vývojářský režim na ostrém serveru povolit:

$configurator->setDebugMode('23.75.345.200');

Jenže IP adresy se mohou měnit a dostane ji někdo jiný. Proto je nově možné přidat ještě pojistku v podobně cookie. Do cookie nazvané nette-debug si uložíte tajný řetězec (buď funkcí setcookie nebo pomocí vývojářského nástroje prohlížeče, každopádně nezapomeňte na příznak httpOnly), například mysecret a necháte Configurator, aby ověřoval i jej. Teprve sedí-li IP adresa i hodnota v cookie, bude aktivován vývojářský režim:

$configurator->setDebugMode('mysecret@23.75.345.200');

Čtvrtá novinka ze tří se týká DI kontejneru a je velmi dobře skrytá. Dovoluje nastavit, které třídy vynechat z autowiringu. Typickým kandidátem je Nette\Application\UI\Control, kterého vám může DI kontejner cpát třeba do konstruktoru formuláře. Seznam ignorovaných tříd předáte metodě ContainerBuilder::addExcludedClasses(). K té se dostanete například v bootstrap.php:

$configurator->onCompile[] = function($configurator, $compiler) {
    $compiler->getContainerBuilder()->addExcludedClasses(array(
        'stdClass',
        'Nette\Application\UI\Control',
    ));
};

A do pětice všeho dobrého: při vývoji můžete narazit na upozornění Possible problem: you are sending a HTTP header while already having some data in output buffer. Try OutputDebugger or start session earlier. To se objeví, když se snažíte odeslat HTTP hlavičky, a ono to sice ještě jde, nicméně aplikace už nějaký výstup předtím odeslala, jen ho zachytil output buffer. V takové situaci je nejlepší nastartovat OutputDebugger a zjistit, odkud se výstup posílal a předejít tomu. Od verze 2.2.3 máte také možnost toto dobře míněné upozornění potlačit pomocí proměnné Nette\Http\Response::$warnOnBuffer. Třeba opět z bootstrapu:

$container->getByType('Nette\Http\Response')->warnOnBuffer = FALSE;

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

Proč Nette musí jít za každou cenu proti proudu a odmítá respektovat standard 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-FIG není obecný standard, je to soubor pravidel, která si tvoří určitá 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 PHP-FIG se shoduje s coding 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 praktičtější umístit do jednoho souboru – jsou tak pěkně pohromadě a např. adresář Utils zůstává 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ří bodů:

  • Nette preferuje oddělovat use deklarace čárkou
  • zatímco PSR vyžaduje psát true, false, null malými písmeny, Nette v souladu s dokumentací PHP používá velká 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.

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.

Jednoduše prostě neznám důvod, proč takhle zasáhnout do kódu a leccos tím zkomplikovat. Není jen jediný standard, tabulátory používá jeden ze tří projektů. Navíc otázka tabulátoru vs mezery je naprosto malicherná. Od toho nás abstrahuje editor.

Jediné podstatné je být konzistentní a v celém kódu dodržovat stejná pravidla.


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.