phpFashion

Na navigaci | Klávesové zkratky

Rubrika PHP

Dokumentační úchylnosti

Dobře udržovaný software má mít kvalitní API dokumentaci. Jistě. Ovšem stejným prohřeškem, jakým je absence dokumentace, je i její přebytečnost. U psaní dokumentačních komentářů je totiž potřeba, podobně jako u návrhu API nebo uživatelského rozhraní, přemýšlet.

Přičemž přemýšlením bych nenazýval proces, který se udál v hlavě vývojáře, když doplnil konstruktor tímto komentářem:

class ChildrenIterator
{
    /**
     * Constructor.
     *
     * @param array $data
     * @return \Zend\Ldap\Node\ChildrenIterator
     */
    public function __construct(array $data)
    {
        $this->data = $data;
    }

Šest řádků, které nepřidaly ani jednu jedinou informaci. Místo toho roste

  • vizuální šum
  • duplicita informací
  • objem kódu
  • možnost chybovosti

Nesmyslnost uvedeného komentáře vám možná připadá evidentní, pak jsem rád. Občas totiž dostávám pull requesty, které se snaží podobné smetí do kódu propašovat. Někteří programátoři dokonce používají editory, které takto znečišťují kód automaticky. Au.

Nebo jiný příklad. Zkuste se zamyslet, zda vám komentář prozradil něco, co by bez něj nebylo zřejmé:

class Zend_Mail_Transport_Smtp extends Zend_Mail_Transport_Abstract
{
    /**
     * EOL character string used by transport
     * @var string
     * @access public
     */
    public $EOL = "\n";

S výjimkou anotace @return lze pochybovat o přínosnosti i v tomto případě:

class Form
{
    /**
     * Adds group to the form.
     * @param  string $caption       optional caption
     * @param  bool   $setAsCurrent  set this group as current
     * @return ControlGroup
     */
    public function addGroup($caption = null, $setAsCurrent = true)

Pokud používáte výmluvné názvy metod a parametrů (což byste měli), pokud ty ještě navíc mají výchozí hodnoty nebo typehinty, nedá vám tento komentář takřka nic. Buď bych ho zredukoval o informační duplicity, nebo naopak rozšířil.

Ale pozor na opačný extrém, jakým jsou romány v phpDoc:

/**
 * Performs operations on ACL rules
 *
 * The $operation parameter may be either OP_ADD or OP_REMOVE, depending on whether the
 * user wants to add or remove a rule, respectively:
 *
 * OP_ADD specifics:
 *
 *    A rule is added that would allow one or more Roles access to [certain $privileges
 *    upon] the specified Resource(s).
 *
 * OP_REMOVE specifics:
 *
 *    The rule is removed only in the context of the given Roles, Resources, and privileges.
 *    Existing rules to which the remove operation does not apply would remain in the
 *    ACL.
 *
 * The $type parameter may be either TYPE_ALLOW or TYPE_DENY, depending on whether the
 * rule is intended to allow or deny permission, respectively.
 *
 * The $roles and $resources parameters may be references to, or the string identifiers for,
 * existing Resources/Roles, or they may be passed as arrays of these - mixing string identifiers
 * and objects is ok - to indicate the Resources and Roles to which the rule applies. If either
 * $roles or $resources is null, then the rule applies to all Roles or all Resources, respectively.
 * Both may be null in order to work with the default rule of the ACL.
 *
 * The $privileges parameter may be used to further specify that the rule applies only
 * to certain privileges upon the Resource(s) in question. This may be specified to be a single
 * privilege with a string, and multiple privileges may be specified as an array of strings.
 *
 * If $assert is provided, then its assert() method must return true in order for
 * the rule to apply. If $assert is provided with $roles, $resources, and $privileges all
 * equal to null, then a rule having a type of:
 *
 *    TYPE_ALLOW will imply a type of TYPE_DENY, and
 *
 *    TYPE_DENY will imply a type of TYPE_ALLOW
 *
 * when the rule's assertion fails. This is because the ACL needs to provide expected
 * behavior when an assertion upon the default ACL rule fails.
 *
 * @param  string                                  $operation
 * @param  string                                  $type
 * @param  Zend_Acl_Role_Interface|string|array  $roles
 * @param  Zend_Acl_Resource_Interface|string|array $resources
 * @param  string|array                          $privileges
 * @param  Zend_Acl_Assert_Interface                $assert
 * @throws Zend_Acl_Exception
 * @uses   Zend_Acl_Role_Registry::get()
 * @uses   Zend_Acl::get()
 * @return Zend_Acl Provides a fluent interface
 */
public function setRule($operation, $type, $roles = null, $resources = null, $privileges = null,
                        Zend_Acl_Assert_Interface $assert = null)

Vygenerovaná API dokumentace je pouhá referenční příručka, nikoliv kniha, kterou by si člověk četl před spaním. Litanie sem skutečně nepatří.

Asi nejoblíbenějším místem, kde se lze dokumentačně vyřádit, jsou hlavičky souborů:

<?php
/**
 * Zend Framework
 *
 * LICENSE
 *
 * This source file is subject to the new BSD license that is bundled
 * with this package in the file LICENSE.txt.
 * It is also available through the world-wide-web at this URL:
 * http://framework.zend.com/license/new-bsd
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to license@zend.com so we can send you a copy immediately.
 *
 * @category   Zend
 * @package Zend_Db
 * @subpackage Adapter
 * @copyright  Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
 * @license http://framework.zend.com/license/new-bsd    New BSD License
 * @version $Id: Abstract.php 25229 2013-01-18 08:17:21Z frosch $
 */

Kolikrát se zdá, že záměrem je hlavičku natáhnout tak, aby po otevření souboru vůbec nebyl vidět kód. K čemu je 10řádková informace o licenci New BSD, obsahující klíčové zvěsti, jako že její znění najdete v souboru LICENSE.txt, že je dostupná přes world-wide-web a pokud náhodou nedisponujete moderními výstřelky, jako je tzv. webový prohlížeč, máte odeslat email na license@zend.com a oni vám ji okamžitě pošlou? Navíc v balíku zopakovaná 4400×. Schválně jsem žádost zkusil poslat, ale odpověď nepřišla :-)

Též uvedení letopočtu v copyrightu vede k vášni dělat komity jako update copyright year to 2014, které změní všechny soubory, což komplikuje porovnávání verzí.

Je vůbec potřeba uvádět v každém souboru copyright? Z právního hlediska to potřeba není, nicméně pokud open source licence dovolují uživatelům používat části kódu s tím, že musí zachovat copyrighty, je vhodné je tam mít. Stejně tak je užitečné v každém souboru uvádět, z jakého produktu pochází, pomůže to lidem v orientaci, když na něj jednotlivě narazí. Dobrým příkladem je třeba:

/**
 * Zend Framework (http://framework.zend.com/)
 *
 * @link      http://github.com/zendframework/zf2 for the canonical source repository
 * @copyright Copyright (c) 2005-2014 Zend Technologies USA Inc. (http://www.zend.com)
 * @license   http://framework.zend.com/license/new-bsd New BSD License
 */

Přemýšlejte proto prosím nad každým řádkem, jestli skutečně má pro uživatele přínos. Pokud ne, jde o smetí, které nemá v kódu co dělat.

(Prosím případné komentátory, aby článek nevnímali jako souboj frameworků, tím rozhodně není.)


How to encode and decode JSON in PHP?

Let's create simple OOP wrapper for encoding and decoding JSON in PHP:

class Json
{
    public static function encode($value)
    {
        $json = json_encode($value);
        if (json_last_error()) {
            throw new JsonException;
        }
        return $json;
    }

    public static function decode($json)
    {
        $value = json_decode($json);
        if (json_last_error()) {
            throw new JsonException;
        }
        return $value;
    }
}

class JsonException extends Exception
{
}

// usage:
$json = Json::encode($arg);

Simple.

But it is very naive. In PHP, there are a ton of bugs (sometime called as “not-a-bug”) that need workarounds.

  1. json_encode() is (nearly) the only one function in whole PHP, which behavior is affected by directive display_errors. Yes, JSON encoding is affected by displaying directive. If you want detect error Invalid UTF-8 sequence, you must disable this directive. (#52397, #54109, #63004, not fixed).
  2. json_last_error() returns the last error (if any) occurred during the last JSON encoding/decoding. Sometimes! In case of error Recursion detected it returns 0. You must install your own error handler to catch this error. (Fixed after years in PHP 5.5.0)
  3. json_last_error() sometimes doesn't return the last error, but the last-but-one error. I.e. json_decode('') with empty string doesn't clear last error flag, so you cannot rely on error code. (Fixed in PHP 5.3.7)
  4. json_decode() returns null if the JSON cannot be decoded or if the encoded data is deeper than the recursion limit. Ok, but json_encode('null') return null too. So we have the same return value for success and failure. Great!
  5. json_decode() is unable to detect Invalid UTF-8 sequence in PHP < 5.3.3 or when PECL implementation is used. You must check it own way.
  6. json_last_error() exists since PHP 5.3.0, so minimal required version for our wrapper is PHP 5.3
  7. json_last_error() returns only numeric code. If you'd like to throw exception, you must create own table of messages (json_last_error_msg() was added in PHP 5.5.0)

So the simple class wrapper for encoding and decoding JSON now looks like this:

class Json
{
    private static $messages = array(
        JSON_ERROR_DEPTH => 'The maximum stack depth has been exceeded',
        JSON_ERROR_STATE_MISMATCH => 'Syntax error, malformed JSON',
        JSON_ERROR_CTRL_CHAR => 'Unexpected control character found',
        JSON_ERROR_SYNTAX => 'Syntax error, malformed JSON',
        5 /*JSON_ERROR_UTF8*/ => 'Invalid UTF-8 sequence',
        6 /*JSON_ERROR_RECURSION*/ => 'Recursion detected',
        7 /*JSON_ERROR_INF_OR_NAN*/ => 'Inf and NaN cannot be JSON encoded',
        8 /*JSON_ERROR_UNSUPPORTED_TYPE*/ => 'Type is not supported',
    );


    public static function encode($value)
    {
        // needed to receive 'Invalid UTF-8 sequence' error; PHP bugs #52397, #54109, #63004
        if (function_exists('ini_set')) { // ini_set is disabled on some hosts :-(
            $old = ini_set('display_errors', 0);
        }

        // needed to receive 'recursion detected' error
        set_error_handler(function($severity, $message) {
            restore_error_handler();
            throw new JsonException($message);
        });

        $json = json_encode($value);

        restore_error_handler();
        if (isset($old)) {
            ini_set('display_errors', $old);
        }
        if ($error = json_last_error()) {
            $message = isset(static::$messages[$error]) ? static::$messages[$error] : 'Unknown error';
            throw new JsonException($message, $error);
        }
        return $json;
    }


    public static function decode($json)
    {
        if (!preg_match('##u', $json)) { // workaround for PHP < 5.3.3 & PECL JSON-C
            throw new JsonException('Invalid UTF-8 sequence', 5);
        }

        $value = json_decode($json);

        if ($value === null
            && $json !== ''  // it doesn't clean json_last_error flag until 5.3.7
            && $json !== 'null' // in this case null is not failure
        ) {
            $error = json_last_error();
            $message = isset(static::$messages[$error]) ? static::$messages[$error] : 'Unknown error';
            throw new JsonException($message, $error);
        }
        return $value;
    }
}

This implementation is used in Nette Framework. There is also workaround for another bug, the JSON bug. In fact, JSON is not subset of JavaScript due characters \u2028and \u2029. They must be not used in JavaScript and must be encoded too.

(In PHP, detection of errors in JSON encoding/decoding is hell, but it is nothing compared to detection of errors in PCRE functions.)


FTP Deployment: nahrávejte přes FTP chytře

Není nic horšího, než uploadovat soubory na FTP ručně, například pomocí Total Commanderu. (Ačkoliv, ještě horší je editovat soubory přímo na serveru a pak se zoufale pokoušet o jakousi synchronizaci.) Jakmile totiž proces nezautomatizujete, stojí vás mnohem víc času a hrozí riziko chyby. Třeba, že některý soubor zapomenete nahrát.

Dnes už se používají sofistikované techniky nasazování aplikací na web, například pomocí Gitu, ale mnoho lidí stále zůstává u nahrávání jednotlivých souborů skrze FTP. Právě pro ně je určen nástroj FTP Deployment, který zautomatizuje a zjednoduší nahrávání aplikací přes FTP.

FTP Deployment je skript napsaný v PHP, který celý proces zautomatizuje. Stačí jen říct, který adresář (local) má kam nahrát (remote). Tyto údaje zapíšete do souboru deployment.ini, jehož odkliknutí můžete rovnou asociovat se spuštěním skriptu, takže deployment se stane věcí jednoho kliknutí:

php deployment deployment.ini

A jak vypadá soubor deployment.ini? Povinná je vlastně jen položka remote, všechny ostatní jsou nepovinné:

; remote FTP server
remote = ftp://user:secretpassword@ftp.example.com/directory
; you can use ftps:// or sftp:// protocols (sftp requires SSH2 extension)

; do not like to specify user & password in 'remote'? Use these options:
;user = ...
;password = ...

; FTP passive mode
passiveMode = yes

; local path (optional)
local = .

; run in test-mode? (can be enabled by option -t or --test too)
test = no

; files and directories to ignore
ignore = "
    .git*
    project.pp[jx]
    /deployment.*
    /log
    temp/*
    !temp/.htaccess
"
; is allowed to delete remote files? (defaults to yes)
allowDelete = yes

; jobs to run before uploading
before[] = local: lessc assets/combined.less assets/combined.css
before[] = http://example.com/deployment.php?before

; jobs to run after uploading and before uploaded files are renamed
afterUpload[] = http://example.com/deployment.php?afterUpload

; directories to purge after uploading
purge[] = temp/cache

; jobs to run after everything (upload, rename, delete, purge) is done
after[] = remote: unzip api.zip
after[] = remote: chmod 0777 temp/cache  ; change permissions
after[] = http://example.com/deployment.php?after

; files to preprocess (defaults to *.js *.css)
preprocess = no

; file which contains hashes of all uploaded files (defaults to .htdeployment)
deploymentFile = .deployment

; default permissions for new files
;filePermissions = 0644

; default permissions for new directories
;dirPermissions = 0755

V testovacím režimu (při spuštění s parametrem -t) k uploadu nebo mazání souborů na FTP nedochází, můžete jej tedy použít k ověření, zda máte všechny hodnoty dobře nastavené.

Položka ignore používá stejný formát jako .gitignore:

  • log – ignoruje všechny soubory či adresáře log, i uvnitř všech podsložek
  • /log – ignoruje soubor či adresář log v kořenovém adresáři
  • app/log – ignoruje soubor či adresář log v podsložce app kořenového adresáře
  • data/* – ignoruje vše uvnitř složky data, ale samotnou služku na FTP vytvoří
  • !data/session – z předchozího pravidla učiní výjimku pro soubor či složku session
  • project.pp[jx] – ignoruje soubory či složky project.ppjproject.ppx

Před započetím uploadu a po jeho skončení můžete nechat zavolat skripty na vašem serveru (viz before a after), které mohou například server přepnout do maintenance režimu, kdy bude odesílat hlavičku 503.

Aby synchronizace i velkého množství souborů proběhla (v rámci možností) transakčně, všechny soubory se nejprve nahrají s příponou .deploytmp a poté, což už je rychlé, přejmenují. Zároveň se na server uloží soubor .htdeployment, kde jsou uloženy md5 otisky všech souborů a právě pomocí něj se nadále web synchronizuje.

Při dalším spuštění tedy nahrává pouze změněné soubory a maže smazané (pokud to nezakážeme direktivou allowdelete).

Nahrávané soubory je možné nechat zpracovat preprocesorem. Standardně jsou nastaveny pravidla, že všechny .css soubory se zkomprimují pomocí Clean-CSS a .js pomocí Google Closure Compiler. Před samotnou komprimací se ještě expandují základní mod_include direktivy Apache. Můžete tedy vytvořit například soubor combined.js:

<!--#include file="jquery.js" -->
<!--#include file="jquery.fancybox.js" -->
<!--#include file="main.js" -->

Který vám bude Apache na lokálním serveru za běhu sestavovat spojením tří uvedených souborů. Říci si o to můžete takto:

<FilesMatch "combined\.(js|css)$">
    Options +Includes
    SetOutputFilter INCLUDES
</FilesMatch>

Přičemž na server se nahraje už ve spojené a zkomprimované podobě. Vaše HTML stránka tak bude šetřit zdroje a načítat jediný JavaScriptový soubor.

V konfiguračním souboru deployment.ini můžete vytvořit i více sekcí, případně si udělat jeden konfigurák zvlášť pro data a jeden pro aplikaci, aby synchronizace byla co nejrychlejší a nemusel se vždy počítat otisk velkého množství souborů.

Nástroj FTP Deployment jsem si vytvořil před mnoha lety a plně pokrývá mé požadavky na deployovací nástroj. Zároveň je třeba zdůraznit, že FTP protokol tím, že přenáší heslo v čitelné podobě, představuje bezpečnostní riziko a rozhodně byste jej neměli používat třeba na veřejných Wi-Fi.


PHP 5.4 má nepoužitelný typ callable

Jednou z novinek PHP 5.4 je typehint callable. Byla by to parádní věc, kdyby to tvůrci tak děsně nezprasili.

PHP typ callbable je pseudotypem, jehož hodnotou může být buď název metody či funkce (tj. řetězec) nebo dvojice třída/objekt a její metoda, tedy pole. V PHP se používá od pradávna, nicméně od verze 5.4 pro něj existuje typehint:

class Template
{
    function registerHelper($name, callable $helper)
    {
        ...
    }
}

$template = new Template;
$template->registerHelper('date', 'Helpers::date');

Skvělé. Tedy až do chvíle, než to vyzkoušíte a PHP vás zdupne:

error:  Argument 2 passed to Template::registerHelper() must be callable, string given

Aha, tak je to asi potřeba zapsat jako pole:

$template->registerHelper('date', ['Helpers', 'date']);

Výsledek je opět:

error: Argument 2 passed to Template::registerHelper() must be callable, array given

Ve skutečnosti PHP vadí to, že třída Helpers neexistuje. Totiž už během volání registerHelper její přítomnost vyžaduje. Typehint callable, narozdíl od všech jiných typehintů, neověřuje jen formální platnost předaného argumentu, ale ujišťuje se, že uvedená třída skutečně existuje a má zmíněnou metodu. Pokud třída neexistuje, pokusí se ji načíst autoloadingem.

Což jednak zabíjí lazyloading – už samotné předání parametru s callbackem načte příslušnou třídu, která by se třeba jinak vůbec načítat nemusela.

A za druhé to generuje naprosto idiotské chybové hlášky. Což je věc, na kterou jsem dosti citlivý. Ze zprávy must be callable, string given těžko někdo pochopí, že typ string je použitý správně, jen neexistuje třída či metoda. Já bych to chápal tak, že funkce neakceptuje řetězce. Přitom třeba funkce call_user_func ve stejné situaci generuje zcela smysluplné chyby jako:

Warning: call_user_func() expects parameter 1 to be a valid callback, class 'Helpers' not found

Warning: call_user_func() expects parameter 1 to be a valid callback, class 'Helpers' does not have a method 'date'

Na chybu jsem upozornil, bohužel Rasmus je se současným chováním nadmíru spokojen. Setrvávat na zavádějících chybových hláškách je projevem arogance a hlouposti: namísto opravy raději uvedou ve zmatek statisíce programátorů a připraví je o spoustu času. Nette Framework rozhodně zprasený typ callable používat nebude.


Monkey patching v PHP

PHP přistupuje ke třídám způsobem známým ze staticky typovaných jazyků a neumožňuje monkey patching, tedy měnit za běhu metody tříd, kopírovat je mezi instancemi a podobně.

Abyste porozuměli, co mám na mysli, vytvořme třídu Greeting s metodou say():

class Greeting
{
    function __construct($name)
    {
        $this->name = $name;
    }

    function say($message)
    {
        echo "$message $this->name.";
    }
}

$g = new Greeting('John');
$g->say('Hello'); // Hello John.

V PHP neočekáváme, že by bylo možné metodu třeba uložit do proměnné či jiného atributu a poté zase zavolat:

$method = $g->say;
$g->greet = $method;
$g->greet('Hello');

Nebo dokonce za chodu přidávat metody nové:

$g->shout = function($message) {
    echo "$message $this->name!!!";
};

$g->shout('Hello'); // Hello John!!!

Na jedné straně je mi líto, že tohle PHP neumí, na straně druhé vidím ve statickém pojetí tříd podstatné výhody. A na straně třetí: v PHP lze tohle chování snadno emulovat.

Emulace dynamiky

Vytvořit funkci jako je výše uvedená shout() a vložit ji do proměnné objektu PHP už umí od verze 5.3. Ale abychom ji mohli zavolat běžným zápisem, musíme si vypomoci magickou metodou __call():

class Greeting
{
    function __call($name, $args)
    {
        if (!isset($this->$name) || !$this->$name instanceof Closure) {
            throw new Exception("Method $name not found.");
        }
        return call_user_func_array($this->$name->bindTo($this, $this), $args);
    }

    ...
}

A nyní už bude příklad s metodou shout() fungovat.

Abychom mohli stejně nakládat i se statickými metodami, jako byla třeba výše uvedená say(), doplníme ještě __get():

class Greeting
{
    function __get($name)
    {
        if (!method_exists($this, $name)) {
            throw new Exception("Property $name not found.");
        }
        return function() use ($name) {
            return call_user_func_array(array($this, $name), func_get_args());
        };
    }

    ...
}

A nyní bude fungovat i první příklad s přiřazením $method = $g->say a následným voláním.

Pro verzi 5.3

Uvedené příklady vyžadují PHP 5.4. Ve verzi 5.3 jsou closures ořezané a nesmí se v nich používat $this. Řešení by vypadalo trošičku jinak:

// for PHP 5.3

class Greeting
{
    function __call($name, $args)
    {
        if (!isset($this->$name) || !$this->$name instanceof Closure) {
            throw new Exception("Method $name not found.");
        }
        array_unshift($args, $this);
        return call_user_func_array($this->$name, $args);
    }

    function __get($name)
    {
        if (!method_exists($this, $name)) {
            throw new Exception("Property $name not found.");
        }
        return function() use ($name) {
            $args = func_get_args();
            return call_user_func_array(array(array_shift($args), $name), $args);
        };
    }

    ...
}

A namísto $this bychom uvnitř closure použili první argument, pojmenovaný třeba $self.

$g->shout = function($self, $message) {
    echo "$message $self->name!!!";
};

Nicméně narozdíl od 5.4 varianty má nyní funkce přístup jen k veřejným proměnným třídy.


Víte, co znamená $ v regulárním výrazu?

Nejprve otázka: mačne nebo nemačne?

$str = "123\n";

echo preg_match('~^\d+$~', $str);

Kdo z vás si myslí, že funkce vrátí false, protože regulární výraz běží v jednořádkovém režimu a nepovoluje v řetězci žádné jiné znaky krom číslic, ten se mýlil.

Malinko odbočím. Regulární výrazy v jazyce Ruby mají jednu nectnost (nesoulad s de facto standardem PERLu): znaky ^ a $ neoznačují začátek a konec řetězce, ale jen jednoho řádku v něm. Neznalost tohoto faktu může způsobit bezpečnostní zranitelnost, jak třeba upozorňuje dokumentace Rails. PHP se chová standardně, ale málokdo už ví, co přesně ono standardní chování znamená. Dokumentace meta-znaku $ je totiž nepřesná. (už opraveno)

Správně má být, že znak $ znamená konec řetězce nebo ukončující odřádkování; ve víceřádkovém režimu (modifikátor m) znamená konec řádku.

Skutečný konec řetězce chytá sekvence \z. Nebo je možné použít dolar společně s modifikátorem D.

$str = "123\n";
echo preg_match('~^[0-9]+$~', $str); // true
echo preg_match('~^[0-9]+$~D', $str); // false
echo preg_match('~^[0-9]+\z~', $str); // false

DI a property injection

Dependency Injection je zřejmé předávání závislostí, tedy že se každá třída otevřeně hlásí ke svým závislostem, místo toho, aby je někde pokoutně získávala. Co kdyby se závislosti předávaly přímo do proměnných? Proberu úskalí a výhody property injection.

Property injection má jednu podstatnou výhodu: stručnost. Srovnejte:

class Foobar
{
    /** @var HttpRequest */
    private $httpRequest;

    /** @var Router */
    private $router;

    function __construct(HttpRequest $httpRequest, Router $router)
    {
        $this->httpRequest = $httpRequest;
        $this->router = $router;
    }

}

versus

class Foobar
{
    /** @var HttpRequest @inject */
    public $httpRequest;

    /** @var Router @inject */
    public $router;

}

Proměnné musíme definovat tak jako tak. Zpravidla u nich uvádíme i anotaci @var a příslušný datový typ. Je lákavé si ušetřit práci a místo psaní rutinního kódu konstruktoru nebo metody inject() doplnit prosté @inject. Property injection kromě minimálního režijního kódu navíc parádně řeší problém s předáváním závislostí a dědičností.

Použití anotace představuje jinou konvenci pro předání závislostí. Zdůrazňuji slovo jinou, protože ať už vyjmenujeme závislosti jakožto argumenty metody nebo anotováním, jde o ekvivalentní činnost. Čímž oponuji názoru, že použití anotace představuje závislost na kontejneru. To v žádném případě není pravda, jde jen o konvenci, koneckonců dosud o kontejnerech nepadla řeč a ukázky dávají smysl.

Stejně tak se nedívejte na anotaci @inject jako nějakou odpornou magii, kterou musíte nastudovat, abyste ji mohli používat. Žádná magie tu není. Jde o obyčejné veřejné proměnné a anotace je jen doplňující informace pro programátora, říkající, že objekt vyžaduje tyto proměnné naplnit. (Nutno dodat, že Jakub Vrána reagoval na použití anotací u private proměnných, což magie je.)

V článku o předávání závislostí jsem se používání proměnných širokým obloukem vyhnul, protože mají vážné nedostatky:

  • public proměnné nezajistí typovou kontrolu
  • public proměnné nezajistí neměnnost
  • private proměnné nelze naplnit žádnou jazykovou konstrukcí
  • private proměnné nejsou součástí veřejného API – nejde tedy o deklaraci závislosti!
  • pro protected proměnné platí nevýhody obou

Ještě bych přidal, že anotace nejsou nativní součástí jazyka PHP a jde tedy o nestandardní konvenci, oproti třeba injektáži přes konstruktor.

Poznámka: vstřikování závislostí do privátních proměnných posvětila třeba Java EE 6 a je to skutečně ee. Třída své závislosti tají (private = neveřejný) a nelze ji instancovat jinak, než kontejnerem (závislost na kontejneru). Jde zcela proti smyslu Dependency Injection, jak je popsán v perexu tohoto článku, a také proti základnímu principu OOP, zapouzdření. Označil bych to jako „Inversion of Dependency Injection.“

Pro properly property injection bychom potřebovali once-write-only veřejnou proměnnou s typovou kontrolou. Kdyby tohle PHP umělo, nic by nebránilo je používat. Jenže PHP to neumí.

Emulace inject property

PHP to neumí, ale lze to emulovat!

Emulaci zajistíme pomocí magických metod __set a __get. Jak ale dosáhnout toho, aby se k public proměnné přistupovalo skrze tyto metody? Použijeme trik: v konstruktoru ji unsetneme. Proměnná zmizí a při přístupu k ní se již použijí magické metody.

Příklad implementace ve formě základní třídy Object by mohl vypadat třeba takto:

class Object
{
    private $injects = array();

    function __construct()
    {
        // následující analýza proměnných by se mohla kešovat
        $rc = new ReflectionClass($this);
        foreach ($rc->getProperties() as $prop) {
            if ($prop->isPublic() && strpos($prop->getDocComment(), '@inject')
                && preg_match('#@var\s+(\S+)#', $prop->getDocComment(), $m)
            ) {
                // unset property to pass control to __set() and __get()
                unset($this->{$prop->getName()});
                $this->injects[$prop->getName()] = array('value' => null, 'type' => $m[1]);
            }
        }
    }


    function __set($name, $value)
    {
        if (!isset($this->injects[$name])) {
            throw new Exception("Cannot write to an undeclared property $$name.");

        } elseif ($this->injects[$name]['value']) {
            throw new Exception("Property $$name has already been set.");

        } elseif (!$value instanceof $this->injects[$name]['type']) {
            throw new Exception("Property $$name must be an instance of {$this->injects[$name]['type']}.");

        } else {
            $this->injects[$name]['value'] = $value;
        }
    }


    function __get($name)
    {
        if (!isset($this->injects[$name])) {
            throw new Exception("Cannot read an undeclared property $$name.");
        }
        return $this->injects[$name]['value'];
    }

}

pak stačí deklarovat výše uvedenou třídu Foobar jako potomka Object a vše bude fungovat standardně podle očekávání:

class Foobar extends Object
{
    /** @var HttpRequest @inject */
    public $httpRequest;

    /** @var Router @inject */
    public $router;

}

$fb = new Foobar;
$fb->router = new Router;

Navíc však máme zajištěnou neměnnost a typovou kontrolu:

$fb->router = new Router;
// Exception: Property $router has already been set.

$fb->httpRequest = new Router;
// Exception: Property $httpRequest must be an instance of HttpRequest.");

Čistá cesta nebo prasárna?

Zkusme se zamyslet nad tím, co vlastně anotace @inject představuje: hint pro programátora, že proměnnou má při vytváření objektu nastavit a že ji později nesmí měnit. Anotace @var pak nařizuje typ.

Je na programátorovi, aby dodržel kontrakt. Stejně jako v případě anotace @private v PHP 4 nebo JavaScriptu, či anotace @return v současném PHP. Jde o pravidla, u nichž se předpokládá, že je programátor dodrží, aniž to lze na úrovni interpreteru ověřit.

Třída Object rozšiřuje PHP o schopnost kontroly za běhu, usnadní tedy identifikaci chyb. Je to vychytávka navíc. Z mého pohledu tedy akceptovatelná cesta k použití property injection v PHP. Možná by se dalo uvažovat nad zařazením do Nette\Object a legitimizace této injektáže v Nette.



DI a předávání závislostí

Víte, že Dependency Injection je zřejmé předávání závislostí, tedy že se každá třída otevřeně hlásí ke svým závislostem, místo toho, aby je někde pokoutně získávala. Otázka zní, jak se k nim hlásit a jak je předávat.

K předávání závislostí můžeme využít konstruktor:

class Foobar
{
    private $httpRequest, $router, $session;

    function __construct(HttpRequest $httpRequest, Router $router, Session $session)
    {
        $this->httpRequest = $httpRequest;
        $this->router = $router;
        $this->session = $session;
    }

}

$foobar = new Foobar($hr, $router, $session);

Nebo metody:

class Foobar
{
    private $httpRequest, $router, $session;

    function setHttpRequest(HttpRequest $httpRequest)
    {
        $this->httpRequest = $httpRequest;
    }

    function setRouter(Router $router)
    {
        $this->router = $router;
    }

    function setSession(Session $session)
    {
        $this->session = $session;
    }
}

$foobar = new Foobar;
$foobar->setSession($session);
$foobar->setHttpRequest($hr);
$foobar->setRouter($router);

Nebo přímo naplnit jednotlivé proměnné:

class Foobar
{
    /** @var HttpRequest */
    public $httpRequest;

    /** @var Router */
    public $router;

    /** @var Session */
    public $session;
}

$foobar = new Foobar;
$foobar->session = $session;
$foobar->httpRequest = $hr;
$foobar->router = $router;

Které řešení je nejlepší? Aby článek nebyl neúměrně dlouhý, odkážu se na Předávání závislostí od Vaška Purcharta, tak si jej přečtěte, protože budu navazovat tam, kde končí.

Takže znovu: které řešení je nejlepší? Kdyby byly ekvivalentní, bylo by nejspíš to poslední, protože kód třídy je nejkratší a kratší kód minimalizuje možnost vzniku chyby a šetří čas při psaní i čtení. Nicméně řešení ekvivalentní nejsou. Jsou spíše diametrálně odlišené.

Immutability tedy neměnnost

Immutable object je objekt, který nemění svůj stav od chvíle, co byl vytvořen. Nevěřili byste, kolik problémů objektového návrhu se dá vyřešit jen tím, že se objekty stanou neměnné. Ale to je téma na jiný článek.

Prakticky vždy budeme chtít, aby závislosti objektu byly neměnné. A v tomto směru se jednotlivé varianty předávání liší. Veřejné (public) proměnné můžeme změnit kdykoliv a změnu nelze detekovat, což je zcela diskvalifikuje ze hry a dále už s touto variantou nebudu vůbec počítat. A to ani nemluvím o chybějící typové kontrole. (Viz také úvaha nad tím, jak by se dalo property injection řešit.)

Mohlo by vás napadnout nahradit public za private a vložit do nich závislosti některým z nízkoúrovňových triků (třeba pomocí reflexe), ale takové obcházení vlastností jazyka do obecných úvah o DI nepatří. Privátní proměnné nejsou součástí veřejného API třídy a nelze se jimi hlásit k závislostem. A také nehackujme jazyk, dokud to není nutné.

Neměnnost bychom si u metod mohli zajistit sami:

function setRouter(Router $router)
{
    if ($this->router) {
        throw new InvalidStateException('Router has already been set.');
    }
    $this->router = $router;
}

A protože metoda není klasický obecný setter, tj. lze ji volat jen jednou, nelze očekávat existenci getteru a můžeme její volání považovat za povinné, mohla by používat jiné názvosloví. Například prefix inject, v tomto případě injectRouter().

Vytvořili bychom tedy pro větší srozumitelnost konvenci, že závislosti předáváme metodami inject.

(Musím zdůraznit, že se bavíme o konvenci užitečné pro programátora, o žádných DI kontejnerech v článku nepadlo ani slovo. Pochopitelně by se jí dalo s úspěchem využít i v kontejnerech, nicméně je naprosto zásadní uvědomit si, co je příčinou a co důsledkem.)

Používání metod pro injektáž má svá úskalí:

  • musíme sami zajistit neměnnost
  • špatně se odhaluje okamžik, kdy jsou nastaveny všechny závislosti, abychom provedli inicializaci objektu
  • měli bychom také ověřovat, že se některé závislosti neopomněly nastavit
  • režijní kód bude poměrně ukecaný a dlouhý

Všechny tyto nedostatky řeší už z principu injektáž přes konstruktor, proto vychází jako nejvhodnější.

(…Tedy, ehm, neřeší… Ale k tomu se hnedle dostaneme.)

Constructor hell

Nenápadný problém předávání závislostí přes konstruktor tkví v tom, že nemáme žádné vodítko, v jakém pořadí jsou parametry uvedeny. Napadá mě snad leda řadit je abecedně (divné, co?). Pokud by dvě závislosti byly stejného typu, potom v pořadí source, destination apod.

Byť nám s tímto problémem může pomoci napovídání v IDE nebo automaticky generované kontejnery, nic to nemění na tom, že metoda s nejasnými parametry snižuje srozumitelnost kódu.

Jakožto líny člověk neoblibuji ani ty strojově se opakující přiřazování v těle konstruktoru. Jako zkratku lze použít:

class Foobar
{
    private $httpRequest, $router, $session;

    function __construct(HttpRequest $httpRequest, Router $router, Session $session)
    {
        list($this->httpRequest, $this->router, $this->session) = func_get_args();
    }

}

Ale pokud by byla poslední závislost nepovinná, mohlo by to skončit u Notice: Undefined offset.

Uvažuji nad sepsáním RFC pro PHP, aby bylo možné používat zápis:

class Foobar
{
    private $httpRequest, $router, $session;

    function __construct(HttpRequest $this->httpRequest, Router $this->router, Session $this->session)
    {
    }

}

Nicméně tohle jsou jen syntaktické libůstky oproti kruciálnímu problému s dědičností.

Co se stane, když vytvoříme potomka:

class Barbar extends Foobar
{
    private $logger;

    function __construct(HttpRequest $httpRequest, Router $router, Session $session, Logger $logger)
    {
        parent::__construct($httpRequest, $router, $session);
        $this->logger = $logger;
    }

}

Jak vidno, konstruktor potomka musí:

  • vyjmenovat závislosti rodiče
  • zavolat rodičovský konstruktor

To je v pořádku, závislosti rodiče jsou i jeho dědictvím. Jenže neexistuje mechanismus, kterým by se dalo volání rodičovského konstruktoru vynutit. Jednou z nejprotivnějších chyb se tak stane opomenutí volání parent::__construct. Takže předpoklad, že konstruktor už z principu vynucuje předání závislostí, je vlastně chybný. Konstruktor se dá snadno obejít.

Bez zajímavosti není, že zdáním je i neměnnost, protože nic nebrání zavolat na hotovém objektu $barbar->__construct(...) a protlačit mu jiné závislosti. Měl by tedy konstruktor testovat, zda není volán podruhé? Kašlete na to, konstruktor se prostě znovu volat nesmí. Otázka konvence.

Největší průšvih nastane ale ve chvíli, kdy provedu refactoring třídy Foobar, jehož důsledkem bude změna závislostí. Bude nutné přepsat konstruktory všech potomků. Jistě, je to logické, ale v praxi může jít o fatální zádrhel. Pokud například rodičem bude třída z frameworku (např. Presenter), jejíž potomky píše každý uživatel frameworku, fakticky se tak znemožní její vývoj, protože zásah do závislostí by byl kolosálním BC breakem.

Řada z výhod konstruktorové injektáže se rozplynula jak pára nad hrncem. Pokud se zdálo, že volání konstruktoru je vynuceno jazykem (silné a bezpečné), zatímco volání metod inject jen konvencí (opomenutelné), tak najednou se ukazuje, že to není zcela pravda.

Další možnosti

Možností, která částečně obchází problém konstruktoru a dědičnosti, je použití třídy FooDependencies zmíněné v článku Dependency Injection versus Service Locator:

class FoobarDependencies
{
    function __construct(HttpRequest $httpRequest, Router $router, Session $session)
}

class Foobar
{
    function __construct(FoobarDependencies $deps)
}

class Barbar extends Foobar
{
    function __construct(FoobarDependencies $deps, Logger $logger)
    {
        parent::__construct($deps);
        $this->logger = $logger;
    }
}

Když se změní závislosti rodičovské třídy Foobar, nemusí to nutně rozbít všechny potomky, protože se předávají v jedné proměnné. Běda ale, pokud ji předat zapomenou… Navíc tento způsob vyžaduje největší množství režijního kódu (dokonce celou režijní třídu).

Nebo lze závislosti rodičovské třídy Foobar předávat metodami a konstruktor uvolnit pro potomky. Rodič by se pak fakticky inicializoval až po volání těchto metod, takže konstruktor potomka by se volal nad neinicializovaným objektem. To není dobré.

A co obráceně, závislosti rodičovské třídy Foobar předávat konstruktorem a potomka metodami? To eliminuje všechny problémy, krom toho, že se těžko odhalí okamžik, kdy jsou nastaveny všechny závislosti (kvůli inicializaci objektu) a zda jsou vůbec nastaveny.

A co kdyby se všechny závislosti potomka nastavily jedinou metodou inject()? To by nejspíš vyřešilo všechny komplikace.

Nicméně stále jde jen o dvojstupňový případ rodič – potomek. Pro každého dalšího potomka by bylo třeba přijít s novou injektovací metodou a byl by problém zajistit, aby byly volány ve správném pořadí.

Dovedu si proto představit, že by vzniklo nové čisté řešení využívající nějaké PHP magie uvnitř třídy, která by ušetřila psaní režijního kódu, elegantně exponovala závislosti a předávala je do proměnných. Ty by mohly být označené třeba anotací @inject, nicméně šlo by o anotaci určenu pro tuto vnitřní implementaci, nikoliv o hint pro DI kontejner. Efekt by to mělo ve chvíli, kdyby se z toho stala obecněji uznávaná konvence, jinak to bude jen magie.

tl;dr

Předávání závislostí různými cestami má svá úskalí. Použití metod vyžaduje velké množství režijního kódu. Není od věci tyto metody pojmenovávat jiným prefixem, než obecné settery, kupříkladu lze použít inject. Poskytne to totiž důležitou informaci pro programátora, sekundárně ji může využít i DI kontejner.

Pokud nepoužíváte dědičnost, je zpravidla nejšikovnější závislosti předat skrze konstruktor a PHP by mohlo v příštích verzích syntaxi ještě o něco zjednodušit. Pokud ale do hry vstoupí dědičnost, je najednou všechno jinak. Ukazuje se, že dokonalý obecný mechanismus asi ani neexistuje. Možná by nebylo od věci zkusit nějaký, byť za využití PHP magie, vymyslet.



DI versus Service Locator

Když se mluví o Dependency Injection, bývá zmiňován i service locator, jako jakési zlé dvojče. O co se vlastně jedná?

Dependency Injection jsem v prvním článku popsal jako zřejmé předávání závislostí, tedy že se každá třída hlásí ke svým závislostem v konstruktoru nebo jiné metodě, místo toho, aby je někde v těle pokoutně získávala z globálního přístupového bodu. Dodržování tohoto principu vede ke srozumitelnějšímu a předvídatelnějšímu kódu.

Nicméně na vás číhá nástraha v podobě service locatoru.

Service locator je velechytrý objekt, který umí vrátit veškeré závislosti, které třída potřebuje, nebo i nepotřebuje. Pokud by si všechny třídy předávaly jeden service locator, předaly by si tak v jediném parametru všechny závislosti.

class Authenticator
{
    private $locator;

    function __construct(ServiceLocator $locator)
    {
        $this->locator = $locator;
    }

    function authenticate($user, $pass)
    {
        $database = $this->locator->getDatabase();
        $database->query(...)
    }
}

Bohužel Service Locator není v souladu s DI.

Proč? Není v souladu s tím, že předávání závislostí je zřejmé a že se třída ke svým závislostem otevřeně hlásí. Třída Authenticator

  • potřebuje databázi, ale hlásí se k velmi obecnému service locatoru, což je v naprostém nepoměru vůči tomu, co skutečně potřebuje
  • že potřebuje zrovna databázi se nedá zjistit jinak, než zkoumáním její implementace

Třída se tedy musí hlásit ke všem svým závislostem a právě jen k nim. Jinak o svých závislostech lže.

(Může nastat situace, kdy požadovat service locator je korektní: pokud ho třída skutečně jako takový potřebuje. Třeba kvůli jeho vizualizaci apod.)

Co naopak service locator není

Občas někdo pojmenuje validní konstrukci termínem service locator a na základě toho ji odsoudí. Podobný styl uvažování je zavádějící. Znamená, že něco používáme či zavrhujeme a už nevím proč vlastně.

Důležité je si uvědomit, co je tím špatným na service locatoru, tj. proč jej řadíme mezi antipatterny, a ověřit, jestli naše konstrukce netrpí stejnými vadami. Tedy diskuse o tom, zda jde o service locator nebo ne, je pak podružná.

Jako příklad si ukažme refaktoringu třídy Foo se třemi závislostmi:

class Foo
{
    function __construct(A $a, B $b, C $c)
}

$foo = new Foo($a, $b, $c);

Závislosti vytkneme do jedné (immutable) třídy:

class FooDependencies
{
    function __construct(A $a, B $b, C $c)
}

class Foo
{
    function __construct(FooDependencies $d)
}

$foo = new Foo(new FooDependencies($a, $b, $c));

Z hlediska DI jsou obě alternativy korektní, třídy se hlásí ke všem svým závislostem a právě jen k nim. Neplatí tu námitky proti service locatoru. Samozřejmě je otázka, zda uvedený refactoring byl opodstatněný a správný, ale to už je jiný příběh.

Obdobně v úvodním článku Co je Dependency Injection, kde jsem skupinu závislostí třídy Article v presenteru zredukoval na továrničkou ArticleFactory, nejsou na místě obavy, že továrnička je service locator. Nevykazuje totiž jeho negativní rysy. Stejně tak i příklad v článku Dependency Injection versus Lazy loading, kde Authenticator byl nahrazen za AuthenticatorAccessor, neboť presenter chtěl službu získat skrze lazy loading.

Jak vidno, v DI nejde jen o jakékoliv předávání závislostí. Musí být zřejmé. A pokud si nejste jisti, zda používáte korektní objektový návrh v souladu s DI, udělejte si test „slepým API“. Skryjte si těla metod nebo vygenerujte API dokumentaci a závislosti jednotlivých tříd musí být stále jednoznačně patrné.



DI versus Lazy loading

Lazy loading je návrhový vzor, který odkládá vytváření objektů až do okamžiku, kdy je aplikace skutečně potřebuje. Jak to skloubit s Dependency Injection, které naopak rádo objekty získává už v konstruktorech?

Jak jsme si řekli, Dependency Injection je zřejmé předávání závislostí, tedy že se každá třída ke svým závislostem otevřeně hlásí v inicializačních metodách, obvykle přímo v konstruktoru.

Mějme SignPresenter, který zobrazuje a obhospodařuje formulář pro přihlašování se do aplikace. Po odeslání formuláře se zavolá metoda formSubmitted(), která ověří přihlašovací údaje pomocí autentikátoru. Zjednodušený kód by vypadal takto:

class SignPresenter
{
    function formSubmitted()
    {
        // $GLOBALS['authenticator']->authenticate(...)
        Registry::getAuthenticator->authenticate(...)
    }
}

V souladu s principem DI si nebudeme autentikátor čarovat z globálního prostředí, ale přiznáme se k této závislosti v konstruktoru:

class SignPresenter
{
    private $auth;

    function __construct(Authenticator $auth)
    {
        $this->auth = $auth;
    }

    function formSubmitted()
    {
        $this->auth->authenticate(...);
    }
}

V praxi však narazíme na zásadní problém: na jedno odeslání formuláře bude připadat třeba 1000 jeho zobrazení. Nicméně autentikátor se bude inicializovat vždy. A jelikož ověřuje vůči databázi, bude v souladu s DI vyžadovat v konstruktoru objekt PDO, jehož vytvoření způsobí připojení se k databázovému serveru.

Tedy každé zobrazení formuláře bude doprovázeno načtením tříd v 99.9 % případů nepotřebných, vytvářením nepotřebných objektů a zbytečným připojením k databázi.

To je závažný nedostatek, který nám vyřeší právě lazy loading. Jednou z možností je vytvořit tzv. proxy, objekt implementující rozhraní Authenticator a obalující původní autentikátor, který jej ovšem instancuje až při zavolání metody authenticate(). Druhou možností, která však vyžaduje změnu presenteru, je si místo autentikátoru předat továrničku, která nám jej vyrobí později:

class SignPresenter
{
    private $authFactory;
    private $auth;

    function __construct(AuthenticatorFactory $authFactory)
    {
        $this->authFactory = $authFactory;
    }

    function getAuthenticator()
    {
        if (!$this->auth) {
            $this->auth = $this->authFactory->create();
        }
        return $this->auth;
    }

    function formSubmitted()
    {
        $this->getAuthenticator()->authenticate(...);
    }

}

interface AuthenticatorFactory
{
    /** @return Authenticator */
    function create();
}

Metoda getAuthenticator() zajišťuje, že budeme ve třídě SignPresenter pracovat s jedinou instancí autentikátoru.

A tady by mohl článek skončit. Jenže nekončí.

Továrničku nebrat!

Zdálo se vám použití továrničky jako dobrý nápad či jako úplná samozřejmost? Pak na chvíli zbrzděte.

Zkuste se zamyslet nad rozdílem mezi:

  • budu potřebovat objekt získat
  • budu potřebovat objekt vyrobit

My objekt budeme potřebovat získat, což je obecnější formulace než vyrobit.

Vraťme se k prvnímu DI příkladu, kde získáváme autentikátor přes konstruktor. Co říká hlavička konstruktoru? Že se mu má předat autentikátor. Nikoliv, že se má vyrobit a poté předat. Metoda, která vytváří instanci SignPresenter, může autentikátor získat jakýmkoliv způsobem (a třeba jej vyrobit), ale samotnému presenteru je po tom kulový. Ten ho jen vyžaduje a neptá se po původu.

Jenže řešení s továrničkou kromě podpory lazy loadingu předjímá i způsob získávání objektu: bude se vyrábět. Takže zatímco v prvním případě dostává SignPresenter jeden autentikátor, ve druhém případě dostává nástroj, kterým si může vyrobit autentikátorů více. Ale o to nám nešlo. Nestojíme před potřebou vyrábět autentikátory, řešíme potřebu lazy-loadingu jediného autentikátoru.

Zdánlivě korektní nasazení továrny je chybné, za chvíli se k tomu ještě vrátím. Správné řešení je místo továrny předat něco, co nám autentikázor později vrátí (ne nutně vyrobí). Říkejme tomu třeba Getter nebo Accessor (v žádném případě nejde o Service locator):

class SignPresenter
{
    private $authAccessor;

    function __construct(AuthenticatorAccessor $authAccessor)
    {
        $this->authAccessor = $authAccessor;
    }

    function formSubmitted()
    {
        $this->authAccessor->get()->authenticate(...);
    }

}

interface AuthenticatorAccessor
{
    /** @return Authenticator */
    function get();
}

Kód presenteru se nám navíc zjednoduší, protože nepotřebujeme metodu getAuthenticator(). Samotný accessor totiž zajišťuje, že pracujeme stále se stejnou instancí.

Jak AuthenticatorFactory tak AuthenticatorAccessor uvádím jako rozhraní, neboť na implementaci vůbec nezáleží.

Zkusme se podívat, jak může v praxi vypadat kupříkladu testování presenteru:

// vyrobíme si mockovaný autentikátor
$auth = ...;

// a potřebujeme ho dostat do presenteru
$presenter = new SignPresenter(new TrivialAuthenticatorAccessor($auth));

kde TrivialAuthenticatorAccessor je vskutku triviální:

class TrivialAuthenticatorAccessor implements AuthenticatorAccessor
{
    private $instance;

    function __construct(Authenticator $instance)
    {
        $this->instance = $instance;
    }

    function get()
    {
        return $this->instance;
    }
}

Pokud bychom místo accessoru šli původně navrhovanou cestou továrničky, měli bychom docela problém, jak $auth do presenteru propašovat. (Mimochodem ukázkový příklad, jak testování vede k lepšímu návrhu kódu).

Přičemž jakoukoliv továrničku lze do podoby accessoru snadno přetransformovat, například pomocí obecného CallbackAccessor:

abstract class CallbackAccessor
{
    private $instance;
    private $callback;

    function __construct(/*callable*/ $callback)
    {
        $this->callback = $callback;
    }

    function get()
    {
        if (!$this->instance) {
            $this->instance = call_user_func($this->callback);
        }
        return $this->instance;
    }
}

Jakýkoliv callback pak můžeme přetavit do podoby AuthenticatorAccessor:

class CallbackAuthenticatorAccessor extends CallbackAccessor implements AuthenticatorAccessor
{}

$presenter = new SignPresenter(new CallbackAuthenticatorAccessor(function(){
    return ...;
}));

před 7 lety v rubrice PHP


phpFashion © 2004, 2019 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í.