phpFashion

Na navigaci | Klávesové zkratky

How to Mock Final Classes?

How to mock classes that are defined as final or some of their methods are final?

Mocking means replacing the original object with its testing imitation that does not perform any functionality and just looks like the original object. And pretending the behavior we need to test.

For example, instead of a PDO with methods like query() etc., we create a mock that pretends working with the database, and instead verifies that the correct SQL statements are called, etc. More e.g. in the Mockery documentation.

And in order to be able to pass mock to methods that use PDO type hint, it is necessary for the mock class to inherit from the PDO. And that can be a stumbling block. If the PDO or method query() were final, it would not be possible.

Is there any solution? The first option is not to use the final keyword at all. This, of course, does not help with the third-party code that it uses, but mainly detracts from the important element of the object design. For example, there is dogma that every class should be either final or abstract.

The second and very handy option is to use BypassFinals, which removes finals from source code on-the-fly and allows mocking of final methods and classes.

Install it using Composer:

composer require dg/bypass-finals --dev

And just call at the beginning of the test:

require __DIR__ . '/vendor/autoload.php';

DG\BypassFinals::enable();

Thats all. Incredibly black magic :-)

BypassFinals requires PHP version 5.6 and supports PHP up to 7.2. It can be used together with any test tool such as PHPUnit or Mockery.


This functionality is directly implemented in the „Nette Tester“: https://tester.nette.org version 2.0 and can be enabled this way:

require __DIR__ . '/vendor/autoload.php';

Tester\Environment::bypassFinals();

Jak mockovat final třídy?

Jak mockovat třídy, které jsou definované jako final nebo některé z jejich metod jsou final?

Mockání znamená nahrazení původního objektu za jeho testovací imitaci, která neprovádí žádnou funkcionalitu a jen se tváří jako původní objekt. A předstírá chování, které potřebujeme kvůli testování.

Takže například místo objektu PDO s metodami jako query() apod. vytvoříme jeho mock, který práci s databází jen předstírá, a místo toho ověřuje, že se volají ty správné SQL příkazy atd. Více třeba v dokumentaci Mockery.

A aby bylo možné mock předávat metodám, které mají type hint PDO, je potřeba, aby i třída mocku dědila od PDO. A to může být kámen úrazu. Pokud by totiž třída PDO nebo metoda query() byla final, už by to nebylo možné.

Existuje nějaké řešení? První možnost je final vůbec nepoužívat. To ovšem nepomůže s kódem třetích stran, který final používá, ale hlavně se tím ochuzujeme o důležitý prvek objektového návrhu. Existuje dogma, že každá třída by měla být buď final, nebo abstract.

Druhou a velmi šikovnou možností je použít Nette Tester, který od verze 2.0 disponuje vychytávkou, která odstraňuje z kódu klíčové slovo final on-the-fly. Stačí na začátku testu zavolat:

require __DIR__ . '/vendor/autoload.php';

Tester\Environment::bypassFinals();

A je to. Je za tím ukrytá neskutečně černá magie :-)

Pokud nepoužíváte Nette Tester, ale třeba PHPUnit, nebudete ochuzeni, stačí si nainstalovat BypassFinals:

composer require dg/bypass-finals --dev

A na začátku skriptu zavoláte:

require __DIR__ . '/vendor/autoload.php';

DG\BypassFinals::enable();

Jak souhrnně nazývat třídy a rozhraní?

Názvoslovný oříšek: jak souhrnně označovat třídy a rozhraní? Jak třeba nazvat proměnnou, která může obsahovat jak název třídy, tak rozhraní? Co zvolit místo $class?

Dá se tomu říkat type ($type), nicméně to je zase příliš obecné, protože typem je i řetězec nebo pole. Z pohledu jazyka jím může být i něco komplikovanějšího, třeba ?array. Navíc je sporné, co je v případě objektu jeho typ: je jím název třídy, nebo je to object?

Nicméně souhrnné označení pro třídy a rozhraní skutečně existuje: je jím slovo třída.

Cože?

  1. Z pohledu deklarace je interface hodně ořezaná třída. Může obsahovat jen veřejné abstraktní metody. Což také implikuje nemožnost vytvářet objekty. Rozhraní jsou tedy podmnožinou tříd. A pokud je něco podmnožinou, tak to můžeme označovat názvem nadmnožiny. Člověk je savec, stejně jako rozhraní je třída.
  2. Nicméně je tady ještě pohled užití. Třída může dědit jen od jedné třídy, ale může implementovat vícero rozhraní. Nicméně tohle je omezení týkající se tříd, samotné rozhraní za to nemůže. Obdobně: třída nemůže dědit od final třídy, ale přitom final třídu pořád vnímáme jako třídu. A také pokud třída může implementovat víc rozhraní (tj. tříd, viz 1.), stále je vnímejme jako třídy.

A co traity? Ty sem vůbec nepatří, z hlediska OOP jednoduše neexistují.

Tedy problém se společným pojmenováním tříd a rozhraní je vyřešen. Říkejme jim prostě třídy.

classes + interfaces = classes

No jo, ale vznikl tady problém nový. Jak říkat třídám, které nejsou rozhraní? Tedy jejich doplňku. Tomu, co se ještě na začátku článku nazývalo třídy. Nerozhraní? Nebo implementations:-)

To je ještě větší oříšek. To je pořádný ořech. Víte co, raději zapomeňme na to, že rozhraní jsou také třídy, a tvařme se opět, že každý OOP identifikátor je buď třída, nebo rozhraní. Bude to snazší.


PhpStorm a napovídání nad $this->template

Jak se zbavit upozorňování PhpStormu na „undefined fields“ a aktivovat napovídání u objektu $this->template v presenterech?

Tedy jak změnit tento pohled s podtrženým abc a prázdným napovídacím okénkem:

Na tento?

Jednoduše. Stačí do presenteru (například BasePresenter) doplnit tuto anotaci:

/**
 * @property-read \Nette\Bridges\ApplicationLatte\Template|\stdClass $template
 */
abstract class BasePresenter extends Nette\Application\UI\Presenter

Pozn.: v nejnovějších verzích Nette bude tato anotace přímo v kódu.

Třída šablony

Třída stdClass v anotaci je workaround pro PhpStorm, který jinak všechny proměnné považuje za nedefinované. Nicméně zajímavější je si vytvořit třídu se seznamem skutečných proměnných, která šablona má, včetně jejich typů. Může vypadat třeba takto:

class ArticleTemplate
{
    /** @var string */
    public $lang;

    /** @var int */
    public $page;

    /** @var string[] */
    public $menu;

    /** @var Model\Page */
    public $article;
}

a potom ji v anotaci konkrétního presenteru uvedeme namísto stdClass:

/**
 * @property-read ArticleTemplate|\Nette\Bridges\ApplicationLatte\Template $template
 */
final class ArticlePresenter extends Nette\Application\UI\Presenter

Od této chvíle je napovídání perfektní:

Zbývá ještě vyřešit napovídání v šabloně. To zatím Latte plugin neumí, nicméně až to bude umět, mohl by k tomu využívat opět tu stejnou třídu šablony. Mohlo by se například zavést nové makro {template App\Presenters\ArticleTemplate}, kterým by se název třídy předal.


Nette lišta dává tipy na zajímavé články, videa, události

Web Nette prochází redesignem a první vlaštovkou je zbrusu nová horní lišta.

Na ní podobně jako ve vysílání ČT 24 nebo CNN běží tipy na zajímavé články, knihovny, videa, události atd. Díky ní by vám nemělo uniknout nic zajímavého, co se kolem Nette děje.

Pošlete tipy na články, doplňky, videa!

Jakmile administrátoři tip schválí, objeví se na všech webech Nette.

Napsali jste článek, natočili video? Šup tam s ním. Budete mít reklamu na všech webech Nette.


Jak správně updatovat Nette

Potřebujete aktualizovat projekt běžící na Nette 2.0 na nejnovější verzi? Tady je pár tipů, jak na to.

Stoupejte po jednotlivých verzích

Tj. nejprve aktualizujte na Nette 2.1, poté 2.2, a tak dále. Důvod je ten, že pokud se v Nette nějaká vlastnost změní nebo odstraní, děje se tak v postupných krocích trvajících i několik let. Nejprve je vlastnost jako deprecated jen označena v kódu (silently deprecated), v další velké verzi emituje hlášku E_USER_DEPRECATED, ale funkčnost je zachována, a teprve ve třetí verzi je odstraněna.

Nette se snaží, aby ony hlášky byly maximálně srozumitelné a návodné (např. Syntax {!$var} is deprecated, use {$var|noescape} on line 123), abyste mohli zastaralé věci snadno nahradit.

Pokud byste přeskočili několik verzí, mohli byste přijít o tuto důležitou fázi.

Začínejte bez E_USER_DEPRECATED

Před nasazováním nové verze je vhodné nejprve vypnout hlášení chyb E_USER_DEPRECATED:

$configurator->enableDebugger();
error_reporting(~E_USER_DEPRECATED); // všimněte si ~

Nyní můžete vyzkoušet, zda vše funguje jak má, bez upozorňování na zapovězené věci. Pokud vše funguje, hlášky zase povolte a upravte podle nich kód.

Čtěte dokumentaci

Všechny migrační návody najdete v dokumentaci. Pokud byste ve firmě potřebovali s aktualizací pomoci, jsem vám k dispozici.


Nette 2.4: už je za rohem!

Za pár týdnů vyjde nová verze Nette 2.4, nejnabušenější Nette všech dob. Postupně budu přidávat články, kde se dozvíte o všech novinkách, ale ještě předtím…

…by bylo skvělé, kdybyste otestovali, jestli pod ním běží vaše aplikace. Na všech svých webech už mám testovací 2.4 nasazenou a pochopitelně jsem přitom odhalil řadu chybiček, které bylo potřeba opravit. Tak prosím ověřte, jak jsou na tom vaše weby, než vyjde stable.

Verze 2.4 by měla být na 99 % kompatibilní s verzí 2.3. Zároveň upozorňuje, pokud používáte konstrukce, se kterými se už nepočítá do Nette 3.0. Může se stát, že něco z toho je pro vás důležité a chtěli byste funkčnost zachovat. Stačí o tom dát vědět. Ale samozřejmě co nejdříve.

Podrobné vysvětlení všech případů, kdy Nette generuje upozornění, a několika nekompatibilit mezi 2.3 a 2.4 najdete v tomto vláknu na fóru. A velmi stručný přehled novinek můžete zkouknout ve videu z poslední Poslední soboty.


Co nevíte o output bufferingu v PHP

A co se ani v dokumentaci nedočtete, včetně záplaty na bezpečnostní díru a rady, jak zrychlit odezvu serveru a naopak ji nezbrzdit.

Output buffering umožňuje, aby výstup PHP skriptu (především funkcí echo) nebyl okamžitě odeslán do prohlížeče nebo terminálu, ale byl uchováván v paměti (tj. bufferu). Což se hodí k celé řadě věcí.

Zabránění vypisování na výstup:

ob_start();  // zapne output buffering
$foo->bar();  // veškerý výstup jde pouze do bufferu
ob_end_clean();  // buffer smaže a ukončí buffering

Zachytávání výstupu do proměnné:

ob_start();  // zapne output buffering
$foo->render();  // výstup jde pouze do bufferu
$output = ob_get_contents();  // obsah bufferu uloží do proměnné
ob_end_clean();  // buffer smaže a ukončí buffering

Dvojici ob_get_contents() a ob_end_clean() lze nahradit jedinou funkcí ob_get_clean(), z jejíhož názvu se sice vytratilo end, ale skutečně output buffering i vypíná:

$output = ob_get_clean();  // obsah bufferu uloží do proměnné a vypne buffering

V uvedených příkladech se obsah bufferu na výstup vůbec nedostal. Pokud jej naopak na výstup poslat chci, namísto ob_end_clean() jej ukončím funkcí ob_end_flush() . Pro současné získání obsahu bufferu, odeslání na výstup a ukončení bufferování existuje opět zkratka (i včetně chybějícího end v názvu): ob_get_flush().

Buffer lze kdykoliv vyprázdnit i bez nutnosti jej ukončit, a to pomocí ob_clean() (smaže jej) a nebo ob_flush() (pošle jej na výstup):

ob_start();  // zapne output buffering
$foo->bar();  // veškerý výstup jde pouze do bufferu
ob_clean();  // smažu obsah bufferu, ale buffering zůstává aktivní
$foo->render(); // výstup jde stále do bufferu
ob_flush(); // buffer posílám na výstup
$none = ob_get_contents();  // obsah bufferu je nyní prázdný řetězec
ob_end_clean();  // vypne output buffering

Do bufferu se posílá i výstup zapisovaný na php://output, naopak buffery lze obejít zápisem na php://stdout (nebo do STDOUT), což je k dispozici pouze pod CLI, tedy při spouštění skriptů z příkazové řádky.

Zanoření

Buffery je možné zanořovat, takže zatímco je jeden buffer aktivní, dalším voláním ob_start() se aktivuje buffer nový. Tedy ob_end_flush() a ob_flush() neposílají obsah bufferu na výstup, ale do nadřazeného bufferu. A teprve když žádný nadřazený není, posílá se obsah na skutečný výstup, tj. do prohlížeče nebo terminálu.

Proto je důležité buffering ukončit, a to i v případě, že v průběhu nastane výjimka:

ob_start();
try {
    $foo->render();
} finally {  // finally existuje od PHP 5.5
    ob_end_clean(); // nebo ob_end_flush()
}

Velikost bufferu

Buffer může také zrychlit generování stránky tím, že se do prohlížeče nebude odesílat každé jednotlivé echo, ale až větší objem dat (například 4kB). Stačí na začátku skriptu zavolat:

ob_start(null, 4096);

Jakmile velikost bufferu překročí 4096 bajtů (tzv. chunk size), automaticky se provede flush, tj. buffer se vyprázdní a odešle ven. Téhož se dá dosáhnout i nastavením direktivy output_buffering. V CLI režimu se ignoruje.

Ale pozor, spuštění bufferingu bez uvedení velikosti, tedy prostým ob_start(), způsobí, že se stránka nebude neodesílat průběžně, ale až se vykreslí celá, takže server bude naopak působit velmi líně!

HTTP hlavičky

Output buffering nemá žádný vliv na odesílání HTTP hlaviček, ty se zpracovávají jinou cestou. Nicméně díky bufferingu je možné odeslat hlavičky i poté, co se vypsal nějaký výstup, jelikož se stále drží v bufferu. Ovšem jde o vedlejší efekt, na který neradno spoléhat, protože není jistota, kdy výstup překročí velikost bufferu a odešle se.

Bezpečnostní díra

Při ukončení skriptu se všechny neukončené buffery vypíší na výstup. Což lze považovat za nepříjemnou bezpečnostní díru, pokud si například v bufferu připravujete citlivá data, která nejsou určená pro výstup a dojde přitom k chybě. Řešením je použít vlastní handler:

ob_start(function () { return ''; });

Handlery

Na output buffering lze navázat vlastní handler, tj. funkci, která obsah paměti zpracuje před odesláním ven:

ob_start(
    function ($buffer, $phase) { return strtoupper($buffer); }
);
echo 'Ahoj';
ob_end_flush(); // na výstup se dostane AHOJ

I funkce ob_clean() nebo ob_end_clean() vyvolají handler, ale výstup zahodí a ven neposílají. Přičemž handler může zjistit, která funkce je volána a reagovat na to. Používá se k tomu druhý parametr $phase, což je bitová maska (od PHP 5.4):

  • PHP_OUTPUT_HANDLER_START při otevření bufferu
  • PHP_OUTPUT_HANDLER_FINAL při ukončení bufferu
  • PHP_OUTPUT_HANDLER_FLUSH při volání ob_flush() (ale nikoliv ob_end_flush() nebo ob_get_flush())
  • PHP_OUTPUT_HANDLER_CLEAN při volání ob_clean(), ob_end_clean()ob_get_clean()
  • PHP_OUTPUT_HANDLER_WRITE při automatickém flush

Fáze start, final a flush (resp. clean) mohou klidně nastat současně, rozliší se pomocí binárního operátoru &:

if ($phase & PHP_OUTPUT_HANDLER_START) { ... }
if ($phase & PHP_OUTPUT_HANDLER_FLUSH) { ... }
elseif ($phase & PHP_OUTPUT_HANDLER_CLEAN) { ... }
if ($phase & PHP_OUTPUT_HANDLER_FINAL) { ... }

Fáze PHP_OUTPUT_HANDLER_WRITE nastává jen tehdy, pokud má buffer velikost (chunk size) a ta byla překročena. Jedná se tedy o zmíněný automatický flush. Jen pozor, konstanta PHP_OUTPUT_HANDLER_WRITE má hodnotu 0, proto nelze použít bitový test, ale:

if ($phase === PHP_OUTPUT_HANDLER_WRITE) { .... }

Handler nemusí podporovat všechny operace. Při aktivaci funkcí ob_start() lze jako třetí parametr uvést bitovou masku podporovaných operací:

  • PHP_OUTPUT_HANDLER_CLEANABLE – lze volat funkce ob_clean() a související
  • PHP_OUTPUT_HANDLER_FLUSHABLE – lze volat funkci ob_flush()
  • PHP_OUTPUT_HANDLER_REMOVABLE – buffer lze ukončit
  • PHP_OUTPUT_HANDLER_STDFLAGS – je kombinací všech tří flagů, výchozí chování

Tohle se týká i bufferingu bez vlastního handleru. Například pokud chci zachytávat výstupu do proměnné, nenastavím flag PHP_OUTPUT_HANDLER_FLUSHABLE a buffer tak nebude možné (třeba omylem) poslat na výstup funkcí ob_flush(). Nicméně lze tak učinit pomocí ob_end_flush() nebo ob_get_flush(), takže to poněkud ztrácí smysl.

Obdobně by měla absence flagu PHP_OUTPUT_HANDLER_CLEANABLE zamezit mazání bufferu, ale opět to nefunguje.

A nakonec absence PHP_OUTPUT_HANDLER_REMOVABLE činní buffer uživatelsky neodstranitelný, vypne se až při ukončení skriptu. Příkladem handleru, který je vhodné takto nastavit, je ob_gzhandler, který komprimuje výstup a tedy snižuje objem a zvyšuje rychlost datového přenosu. Jakmile se tento buffer otevře, odešle HTTP hlavičku Content-Encoding: gzip a veškerý další výstup musí být komprimovaný. Odstranění bufferu by rozbilo stránku.

Správné použití je tedy:

ob_start(
    'ob_gzhandler',
    16000, // bez chunk size by server data neodesílal průběžně
    PHP_OUTPUT_HANDLER_FLUSHABLE // ale ne removable nebo cleanable
);

Komprimaci výstupu můžete aktivovat také direktivou zlib.output_compression, která zapne buffering s jiným handlerem (netuším, v čem konkrétně se liší), bohužel chybí příznak, že má být neodstranitelný. Protože je vhodné komprimovat přenos všech textových souborů, nejen v PHP generovaných stánek, je lepší kompresi aktivovat přímo na straně HTTP serveru.




phpFashion © 2004, 2017 David Grudl | o blogu

Pokud není uvedeno jinak, podléhá obsah těchto stránek licenci Creative Commons BY-NC-ND Creative Commons License BY-NC-ND

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