Weblog o elegantním programování v PHP pro mírně pokročilé
Whether you like PHP or not, go and read the article PHP: a fractal of bad design. It's well written by someone who really knows the language which is not true for most other articles about this topic. And there are numerous facts why PHP is badly designed on many levels. There is almost no FUD so it is also a great source for someone who wants to learn PHP really well (which is kind of sad).
I am surprised that I am able to live with PHP and even like it. Maybe I am badly designed too so that I am compatible with PHP. I was able to circumvent or mitigate most problems so the language doesn't bother me.
Anyway, there are several topics which are inaccurate or I don't agree with them. Here they are with no context so they probably wouldn't make much sense without reading the original article:
===compares values and type… except with objects, where===is only true if both operands are actually the same object! For objects,==compares both value (of every attribute) and type, which is what===does for every other type.
This makes sense to me for two reasons:
=== for this operations looks like a good idea to me. Comparing properties of two objects with different class is not very useful anyway.$a = new stdClass stores something like “object ID 576ab4” into the variable $a. $b = new stdClass stores a different object ID. So these two objects are not identical even if they are of the same class and have the same properties. Contrary, $c = $a stores the same object ID to $c so it is identical to $a.Global variables need a
globaldeclaration before they can be used. This is a natural consequence of the above, so it would be perfectly reasonable, except that globals can’t even be read without an explicit declaration—PHP will quietly create a local with the same name, instead. I’m not aware of another language with similar scoping issues.
This is one of the greatest features of PHP. It is not issue at all as limiting the scope is always a Good Thing™. Really, this feature doesn't limit me at all and leads to programs with better architecture.
There are no references. What PHP calls references are really aliases; there’s nothing that’s a step back, like Perl’s references, and there’s no pass-by-object identity like in Python.
PHP is simpler than other languages in several different areas. But this simplicity doesn't lead to more complex programs harder to understand. It also doesn't lead to longer programs harder to read. So I see no reason to avoid this simplicity.
Constants are defined by a function call taking a string; before that, they don’t exist. (This may actually be a copy of Perl’s
use constantbehavior.)
I loved the fact that most idioms in PHP can be expressed by a function call when I learned the language. And I was quite sad that echo, include and several other constructs are not real functions. We also have global const since PHP 5.3 but it's not a big deal.
Appending to an array is done with
$foo[] = $bar.
I endorse this shortcut and it also makes perfect sense to me: If you don't specify the index then use the automatic one. For those more conservative, we also have array_push.
E_STRICTis a thing, but it doesn’t seem to actually prevent much and there’s no documentation on what it actually does.
Every function issuing E_STRICT has it written in its documentation.
Fatal errors (e.g.,
new ClassDoesntExist()) can’t be caught by anything. A lot of fairly innocuous things throw fatal errors, forcibly ending your program for questionable reasons. Shutdown functions still run, but they can’t get a stack trace (they run at top-level), and they can’t easily tell if the program exited due to an error or running to completion.
new ClassDoesntExist() can degrade gracefully by defining __autoload.
Telling if program exited due to an error is not that hard: register_shutdown_function and check if error_get_last is fatal inside the callback.
Function arguments can have “type hints”, which are basically just static typing. But you can’t require that an argument be an
intorstringorobjector other “core” type, even though every builtin function uses this kind of typing, probably becauseintis not a thing in PHP. (See above about(int).) You also can’t use the special pseudo-type decorations used heavily by builtin functions:mixed,number, orcallback.
It's possible to use callable type hint since PHP 5.4.
Closures require explicitly naming every variable to be closed-over. Why can’t the interpreter figure this out? Kind of hamstrings the whole feature. (Okay, it’s because using a variable ever, at all, creates it unless explicitly told otherwise.)
Again, I consider limiting scope as a good thing.
Closed-over variables are “passed” by the same semantics as other function arguments. That is, arrays and strings etc. will be “passed” to the closure by value. Unless you use
&.
Which is perfectly consistent with the rest of the language (passing arguments, assigning variables).
Because closed-over variables are effectively automatically-passed arguments and there are no nested scopes, a closure can’t refer to private methods, even if it’s defined inside a class. (Possibly fixed in 5.4? Unclear.)
It is really fixed in PHP 5.4 and properly documented.
Exceptions in
__autoloadand destructors cause fatal errors.
As all other uncaught exceptions. What else should uncaught exception cause?
forkandexecare not built in. They come with the pcntl extension, but that isn’t included by default.
exec is built in.
Negative indexing doesn’t work, since
-1is just as valid a key as0.
Sure, all arrays in PHP are associative. So not only negative indexing doesn't work, positive indexing doesn't work either. [0] isn't the first element in the array, it's an element with index 0. You can get elements with ordered indexes by array_values.
Despite how heavily PHP code relies on preserving key order:
array("foo", "bar") != array("bar", "foo") array("foo" => 1, "bar" => 2) == array("bar" => 2, "foo" => 1);I leave it to the reader to figure out what happens if the arrays are mixed. (I don’t know.)
Again, all arrays in PHP are associative, there's nothing like mixed array. And the algorithm for comparing arrays is properly documented.
To make sure nobody uploads PHP files, you just check that they don’t have a
.phpextension. All an attacker has to do is upload a file namedfoo.php.txt; your uploader won’t see a problem, but Apache will recognize it as PHP, and it will happily execute.
Disabling PHP execution by blacklisting extension is extremely stupid. Anybody can allow other extensions in future. Disable engine instead.
While the PHP docs suggest using
SetHandlerto make.phpfiles run as PHP,AddHandlerappears to work just as well, and in fact Google gives me twice as many results for it. Here’s the problem.
This is one of the weakest parts of the article. To rephrase the complaint: “If you use other Apache directive than recommended by PHP documentation which works coincidentally too then you will experience some side-effects conforming to Apache documentation. And PHP is to blame.” Really?
No generic standard database API. Stuff like PDO has to wrap every individual database’s API to abstract the differences away.
PDO is the standard generic database API. It doesn't wrap other APIs (such as MySQLi), it is on the same level as them. It just uses the same low-level library (libmysql or MySQLnd), pretty much same as any other programming language.
includeaccepting HTTP URLs. Likewise.
I agree that it is an embarrassment. Just to clarify that it can be (and it is by default) disabled by allow_url_include since PHP 5.2.
Nedařilo se mi najít aplikaci na telefon pro počítání baseballového skóre, která by mi zcela vyhovovala. A tak jsem si zcela v duchu Not Invented Here během půl hodinky spíchnul vlastní (což byla kratší doba, než jsem předtím strávil hledáním).
Aplikaci jsem napsal v JavaScriptu a není na ní dohromady nic zajímavého. Tedy kromě jedné věci. Některé aplikace mají kromě tlačítek na přidání skóre také tlačítko na jeho odebrání. To pro případ, že se člověk uklepne. Stejně tak vedle přidání autu je jeho odebrání. A tak dále. Uživatelské rozhraní se tím příšerně komplikuje a naopak svádí k chybám, protože se někdy uklepnu a místo přidání skóre ho umylem uberu, takže pak ho musím přidat dvakrát, abych chybu napravil. Některé operace (jako změna směny po třech autech) navíc nejdou vrátit vůbec.
Ne, ne, já potřebuji jednu funkci zastrčenou někde pěkně v rohu, která bude schopná vrátit cokoliv. Prostě klasické undo. Jeho implementace v JavaScriptu je jednoduchá díky tomu, že funkce jsou first-class citizens. Jak vypadá základní myšlenka?
var undoStack = [];
var score = 0;
function addScore() {
score++;
undoStack.push(function () {
score--;
});
}
function undo() {
if (undoStack.length) {
undoStack.pop()();
}
}
Díky viditelnosti proměnných můžeme funkce i snadno skládat dohromady:
var team = 'visitor';
var outs = 0;
var inning = 1;
function addOut() {
var old = outs;
var undoTeam = function () { };
outs++;
if (outs >= 3) {
outs = 0;
if (team == 'visitor') {
team = 'home';
undoTeam = function () {
team = 'visitor';
};
} else {
team = 'visitor';
inning++;
undoTeam = function () {
team = 'home';
inning--;
};
}
}
undoStack.push(function () {
outs = old;
undoTeam();
});
}
Přijďte si o tomto tématu popovídat na školení JavaScript a AJAX.
Většina knihoven pro komunikaci s databází nabízí nějaký způsob vázání proměnných: PDO, Dibi, NotORM a mnohé další. Hlavním smyslem tohoto konceptu je ochrana před SQL Injection.
Problém nastane, pokud uživatel vázání proměnných z neznalosti nebo nevědomosti nepoužije a místo toho data uživatele vloží přímo do dotazu. Např. query("... LIMIT $_GET[limit]") místo query("... LIMIT ?", $_GET["limit"]). Jak se tomu knihovna může bránit?
"LIMIT 1".SafeQueryJak realizovat poslední možnost? Jedna z mála věcí, které jsou v PHP konstantní jsou … konstanty. A to ještě jen ty třídní, globální jdou vytvořit dynamicky: define("QUERY", "... LIMIT $_GET[limit]"). Můžeme tedy API navrhnout tak, že bude dotazy načítat výhradně z třídních konstant. Nemůžeme použít něco jako query(Article::SELECT_ONE), protože zevnitř funkce už samozřejmě nepoznáme, že jsme dostali konstantu a ne dynamicky sestavený řetězec. API tedy bude muset vypadat nějak takhle: query("Article", "SELECT_ONE"). Přijdeme o napovídání IDE a obecně o statickou analýzu, ale získáme neprůstřelný systém.
Kód může vypadat třeba takhle:
<?php class SafeQuery { private $pdo; function __construct(PDO $pdo) { $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $this->pdo = $pdo; } /** Execute constant query * @param string * @param string * @param mixed ... bound parameters * @return PDOStatement * @throws PDOException * @copyright Jakub Vrána, http://php.vrana.cz/ */ function execute($class, $constant) { $args = func_get_args(); $args = array_slice($args, 2); $return = $this->pdo->prepare(constant("$class::$constant")); $return->setFetchMode(PDO::FETCH_ASSOC); foreach ($args as $i => $values) { if (!is_array($values)) { $values = array($i + 1 => $values); } foreach ($values as $key => $val) { $type = (is_int($val) ? PDO::PARAM_INT : (is_bool($val) ? PDO::PARAM_BOOL : (is_null($val) ? PDO::PARAM_NULL : (is_resource($val) ? PDO::PARAM_LOB : PDO::PARAM_STR )))); $return->bindValue($key, $val, $type); } } $return->execute(); return $return; } } ?>
Není moc pěkné, že se v kontruktoru nastavuje způsob ošetřování chyb PDO, ale ušetří nás to šílenosti při zpracování různých variant ošetřování. Třída se hodí pro přechodné období, kdy máme starý kód využívající PDO a nový kód využívající tuto třídu. Pokud se bez PDO obejdeme, tak by bylo lepší si jeho instanci vytvořit až vevnitř, aby naši třídu nikdo nemohl obejít přímým použitím PDO. Tím by se zároveň vyřešila připomínka ze začátku tohoto odstavce.
Asi největší nevýhodou třídy je to, že nedovoluje dynamické sestavování dotazu, něco takového:
<?php $where = array(); $params = array(); if ($_GET["q"] != "") { $where[] = "q = ?"; $params[] = $_GET["q"]; } $sql = "SELECT * FROM t" . ($where ? " WHERE " . implode(" AND ", $where) : ""); ?>
Vyřešit by se to dalo celkem snadno tak, že bychom si vytvořili metodu sestavující dotaz z fragmentů vyjádřených konstantou. Volala by se např takto:
<?php $safeQuery->executeComposite(array( array("T", "SELECT_BASE"), array("T", "SELECT_WHERE_Q"), ), $_GET["q"]); ?>
Dalším nedostatkem je nemožnost dynamicky vložit sloupec – obvykle podle kterého se má řadit. To už by se řešilo hůř. Samozřejmě si můžeme vytvořit tolik konstant, podle kolika sloupců dovolujeme řadit. Pokud jde ale řadit cokoliv podle čehokoliv, tak brzo narazíme.
Protivná je nemožnost pro vázání proměnných použít pole, typicky v dotazu IN (). Když bych to měl vyřešit, tak bych asi úplně opustil vázání proměnných nabízené PDO a přešel k zápisu, který používá Dibi, případně něčemu podobnému. To by zároveň vyřešilo i předchozí nedostatek.
Jak se knihovna používá? Není to zase tak strašné:
<?php class Article { const INSERT = 'INSERT INTO article (title, content) VALUES (:title, :content)'; const SELECT_ONE = 'SELECT title, content FROM article WHERE id = :id'; const SELECT_ALL = 'SELECT * FROM article ORDER BY id LIMIT ?'; } $safeQuery = new SafeQuery($pdo); $safeQuery->execute('Article', 'INSERT', array('title' => 'Test', 'content' => 'Hello World!')); foreach ($safeQuery->execute('Article', 'SELECT_ALL', 2) as $comment) { print_r($comment); } ?>
Vytvořili jsme knihovnu, která se celkem pohodlně používá, dovoluje položit téměř všechny SQL dotazy a především je neprůstřelná proti SQL Injection – její uživatel nemůže nevědomě udělat chybu.
Přijďte si o tomto tématu popovídat na školení Bezpečnost PHP aplikací.
Další termíny školení jsou již na spadnutí. Jak dopadla ta poslední? Posuďte sami z reakcí účastníků:
Díky prakticky zaměřenému školení Michala Špačka jsem se naučil nejen uvažovat o bezpečnosti svých PHP aplikací jako případný útočník ale také provádět testy, které bezpečnost prověří ze všech stran. Po tomto školení se pro mě zabezpečování aplikací stalo zábavou. Školení bylo vysoce profesionální ale nechyběl ani osobní přístup, což je vždy velmi cenné. Školení mohu vřele doporučit jak začátečníkům, tak i zkušeným PHP programátorům.
Mé pocity ze školení Úvod do PHP a Programování v PHP 5 jsou kladné. Oceňuji kvalifikovanost a názornost výkladu, kde jsem i já, jako úplný začátečník v PHP, získal povědomost o širokých možnostech jazyka PHP, včetně jeho zvláštností. Samozřejmě, že za 2 dny ze mě není programátor PHP, ale kurz mě nasměroval a dále už bude záležet na mě, jak využiji těchto vědomostí k jejich plynulému rozšiřování. Věřím, že za čas, se stanu opravdu programátorem v PHP :-)
Školení plně splnilo má očekávání. Kromě samotné odbornosti na úrovni, mě příjemně překvapily Vaše mnohé osobní zkušenosti, které tak ukazují, že bezpečností je třeba se seriózně zabývat.
Se školení jsem byl velmi spokojen. Odborně bylo na velmi vysoké úrovni. Jako učitel bych snad měl jen jednu miniaturní poznámku, že by možná bylo dobré dát nějaká společná cvičení hned po vysvětlení základů na začátku školení. Jinak nejlepší školení co jsem v poslední době absolvoval. Děkuji. P.S. Knihu od pana Vrány už mám objednanou.
Pokud o školení máte také zájem, přihlaste se na příští volný termín.
Jeden můj kamarád dostal při pohovoru (nikoliv do Facebooku) zajímavou otázku: „Naprogramuj JavaScriptovou funkci setTimeout v PHP.“ Jak jsem to slyšel, začala mi hlava šrotovat, vymýšlet řešení a narážet na první komplikace ještě dřív, než jsem vzal do ruky tužku nebo klávesnici. A i když jsem si nejdřív říkal, že je to úloha zaměřená hlavně na technické detaily implementace místo na algoritmizaci (o což by mělo jít víc), tak se v ní algoritmus přeci jen použije. Tím pádem se dá rozebrat jeho časová složitost, která je při přímočaré implementaci špatná, ale dá se vylepšit. Prostě mi z toho nakonec vychází perfektní úloha k pohovoru.
Pokud vás úloha taky zaujala, zkuste si ji nejprve vyřešit sami:
Nevíte si s něčím rady? Nejste si jisti, jestli jste na něco nezapomněli? Podívejte se na nápovědu. Časem se dostaneme i k mému řešení.
setTimeout(f, 0), tak se funkce nespustí hned, ale teprve až když doběhne všechno ostatní. Není tedy potřeba lámat si hlavu s nějakým asynchronním plánováním a stačí všechno dávat do fronty a tu na konci spustit.processSetTimeout? Srabi! register_shutdown_function neznáte?Pokud jste nápovědu zohlednili, podívejte se na příklad, který by váš skript měl umět zpracovat.
<?php Window::setTimeout(function () { echo "Wo"; Window::setTimeout(function () { echo "d!\n"; }, 1000); }, 1000); Window::setTimeout(function () { echo "rl"; }, 1500); echo "Hello "; ?>
Příklad by měl pochopitelně vypsat Hello World!. Mazali jste se místo statických metod s normálními? A není náhodou čas tak trochu globální veličina?
Pokud vám všechno funguje, podívejte se na mé první řešení a porovnejte ho se svým.
<?php class Window { static private $timeouts; static function setTimeout(callable $function, $ms) { if (self::$timeouts === null) { register_shutdown_function('Window::shutdown'); } self::$timeouts[] = [ microtime(true) + $ms / 1e3, $function ]; } /** @internal */ static function shutdown() { while (self::$timeouts) { $val = min(self::$timeouts); $key = array_search($val, self::$timeouts); unset(self::$timeouts[$key]); list($time, $function) = $val; $now = microtime(true); if ($time > $now) { usleep(($time - $now) * 1e6); } $function(); } } } ?>
V řešení jsou použity dvě frajeřinky z PHP 5.4 – typehint callable a zkrácená syntaxe polí. Samozřejmě by to fungovalo i bez nich, ale měl jsem nutkavou potřebu ukázat, že jsem in.
Na řešení je zajímavá jiná věc: Samozřejmě mě nejprve napadlo v metodě shutdown prvky setřídit podle cílového času a projít je cyklem foreach. To by ale narazilo v momentě, když by někdo v jednom timeoutu zaregistroval druhý. Takže se timeout vybírá funkcí min. Ta funguje stejně jako porovnávací operátory i s poli podle dokumentovaného algoritmu.
Taky mě nejprve napadlo čas ukládat do klíče ($timeouts[$time][] = $function), ale to narazí na to, že PHP v klíčích pole dovoluje používat pouze celá čísla nebo řetězce. A vynásobení 1e6 by zase přesáhlo maximální velikost celého čísla.
Jaká je časová složitost algoritmu? Nejprve si ji sami odvoďte. Rozmyslete si, jestli by se nedala vylepšit. Pak porovnejte.
Funkce min musí prohlédnout všechny prvky pole, takže stojí O(N). Zavolá se O(N)–krát, celkem tedy algoritmus běží v čase O(N²). Zlepšit by se to dalo tak, že bychom prvky do pole průběžně zatřiďovali v čase O(log N) a vždy jen vybrali ten nejmenší. Celkem by se čas tedy zkrátil na O(N * log N). V PHP ale nevím o funkci, která by nám v setříděném poli rychle našla nejbližší prvek a vložila vedle něj další. A pokud bychom si to psali sami, tak je otázka, co by se složitostí udělalo volání funkce array_splice, kterou bychom nejspíš použili.
Teď taková objektová odbočka: Jak se zbavit příznaku @internal a udělat metodu shutdown zvenčí neviditelnou? Máte? Já taky.
@internalMetodu shutdown úplně zrušíme a při registraci zavoláme:
<?php register_shutdown_function(function ($timeouts) { // ... původní obsah metody shutdown }, self::$timeouts); ?>
To ale nestačí – pole se v PHP předávají kopií, takže by se ztratila vazba na vlastnost třídy. A funkce register_shutdown_function nepodporuje předávání parametrů referencí. Vyřešit by se to dalo tím, že bychom ze self::$timeouts udělali objekt. Psát se mi to nechce. Alternativou je nastavit $timeouts = &self::$timeouts a poslat to callbacku přes use (&$timeouts).
Teď je čas to „mírně“ upravit: Co kdybychom se nespokojili s tím, že další funkci stačí zavolat, až když doběhne předchozí kód a místo toho bychom ji chtěli spustit co nejdřív, jakmile bude její čas? Prostě takové přerušení. Následující kód by tedy měl opět vypsat Hello World!:
<?php Window::setTimeout(function () { echo " World"; }, 550); foreach (preg_split('~~', "Hello!\n") as $ch) { echo $ch; usleep(100000); } ?>
Troufnete si? Nejspíš to nebude jen drobná změna, ale fundamentálně odlišné řešení. Zkuste si ho napsat:
Možná si říkáte, že něco takového v PHP nejde. Ale jde.
declareŘešení spočívá v použití neobvyklé konstrukce declare(ticks = 1). Kamarád mi ani nevěřil, že v PHP něco takového existuje. Ani já jsem to nikdy smysluplně nepoužil, ale tohle je ideální příležitost:
<?php declare(ticks = 1); class Window { static private $timeouts; static function setTimeout(callable $function, $ms) { if (self::$timeouts === null) { register_tick_function('Window::tick'); } self::$timeouts[] = [ microtime(true) + $ms / 1e3, $function ]; } static function tick() { if (!self::$timeouts) { return; } $val = min(self::$timeouts); $key = array_search($val, self::$timeouts); list($time, $function) = $val; if (microtime(true) > $time) { unset(self::$timeouts[$key]); $function(); } } } ?>
Normální program by se tím příšerně zpomalil, protože volání microtime je dost drahé. Celkové řešení bychom navíc museli sestavit kombinací prvního a druhého, já jsem v ukázce záměrně nechal jen to zajímavé. Ale jako řešení u pohovoru by to myslím stačilo.
Příklad se mi natolik líbil, že ho asi začnu o pohovorů dávat sám.
Starší články naleznete v archivu.