PHP triky

Weblog o elegantním programování v PHP pro mírně pokročilé

PHP: a fractal of not so bad design

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:

Global variables need a global declaration 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 constant behavior.)

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_STRICT is 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 int or string or object or other “core” type, even though every builtin function uses this kind of typing, probably because int is 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, or callback.

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 __autoload and destructors cause fatal errors.

As all other uncaught exceptions. What else should uncaught exception cause?

fork and exec are 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 -1 is just as valid a key as 0.

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 .php extension. All an attacker has to do is upload a file named foo.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 SetHandler to make .php files run as PHP, AddHandler appears 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.

include accepting 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.

Jakub Vrána, Dobře míněné rady, 16.5.2012, diskuse: 16 (nové: 16)

Undo v JavaScriptu

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.

Jakub Vrána, Řešení problému, 14.5.2012, diskuse: 9 (nové: 9)

Znemožnění SQL injection

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?

  1. Mohla by SQL dotaz parsovat a varovat před konstantními hodnotami – to by ale znemožnilo i legitimní dotazy např. s "LIMIT 1".
  2. Další možností je na SQL zcela rezignovat a vytvořit si vlastní jazyk, který bude nejspíš podmnožinou SQL, uživatelé se ho budou muset učit a nakonec v něm stejně nevyjádří všechno. O to se snaží třeba Doctrine, i když její pojetí tento problém vůbec neřeší.
  3. A poslední možností je vynutit, aby dotaz neobsahoval žádná data od uživatele.

SafeQuery

Jak 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;
    }
    
}
?>

Nedostatky

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.

Použití

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);
}
?>

Závěr

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í.

Jakub Vrána, Řešení problému, 2.5.2012, diskuse: 23 (nové: 23)

Ohlasy na školení Michala Špačka

Další termíny školení jsou již na spadnutí. Jak dopadla ta poslední? Posuďte sami z reakcí účastníků:

Jakub Tesárek, senior programátor Cybergenics s.r.o.

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.

Jiří Královec

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 :-)

Roman Štefek

Š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.

Vladimír Ptáček

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.

Jakub Vrána, Školení, 30.4.2012, diskuse: 0 (nové: 0)

Funkce window.setTimeout v PHP

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í.

Jakub Vrána, Řešení problému, 11.4.2012, diskuse: 26 (nové: 26)

Starší články naleznete v archivu.

© 2005-2012 Jakub Vrána. Publikované texty můžete přetiskovat pouze se svolením autora. Ukázky kódu smíte používat s uvedením autora a URL tohoto webu bez dalších omezení Creative Commons. Můžeme si tykat. Skripty předpokládají nastavení: magic_quotes_gpc=Off, magic_quotes_runtime=Off, error_reporting=E_ALL & ~E_NOTICE a očekávají předchozí zavolání mysql_set_charset. Skripty by měly být funkční v PHP >= 4.3 a PHP >= 5.0.