Tutorials
PHP 5.3: Closures
PHP 5.3 brengt ons closures. Hoe werken ze, wat kan je ermee en wat kan je er niet mee?
Pagina 1
Je eerste closure
Javascript, Java (sort of), Python en Ruby, en vele andere talen kende ze al langer, maar nu zitten ze ook in PHP: Closures! Ook bekend als lambda-functies.
Maar we hadden toch al [php]create_function[/php]?
Dat is waar, maar hoe bruikbaar was die nou? Dat was eigenlijk niet meer dan een verkapte eval aanroep. Closures zijn vele malen krachtiger!
Ik gebruikte create_function al nooit, waarom zou ik closures gaan gebruiken?
Sommige functies zijn handiger met een callback. Bijvoorbeeld [php]array_walk[/php], [php]usort[/php] en [php]array_map[/php], maar ook [php]ob_start[/php]. Closures zijn heel geschikt voor het definiëren van functies die je eigenlijk maar op 1 plek nodig hebt, maar toch als functie wil hebben om zo bijvoorbeeld onder de niet-recursieve aart van een while-lus uit te komen. Daar komt nog eens bij dat closures variabelen uit de huidige scope kunnen importeren en gebruiken.
Closures definieren
Je eerste closure!
<?php
$foo = function($x, $y) {
return $x * $y;
};
?>
WTF?!: Let erop dat een closure definiëren vergelijkbaar is met een normaal PHP statement. Daarom moet er een punt-komma volgen na je afsluitende haakje.
Maar we hadden toch al [php]create_function[/php]?
Dat is waar, maar hoe bruikbaar was die nou? Dat was eigenlijk niet meer dan een verkapte eval aanroep. Closures zijn vele malen krachtiger!
Ik gebruikte create_function al nooit, waarom zou ik closures gaan gebruiken?
Sommige functies zijn handiger met een callback. Bijvoorbeeld [php]array_walk[/php], [php]usort[/php] en [php]array_map[/php], maar ook [php]ob_start[/php]. Closures zijn heel geschikt voor het definiëren van functies die je eigenlijk maar op 1 plek nodig hebt, maar toch als functie wil hebben om zo bijvoorbeeld onder de niet-recursieve aart van een while-lus uit te komen. Daar komt nog eens bij dat closures variabelen uit de huidige scope kunnen importeren en gebruiken.
Closures definieren
Je eerste closure!
<?php
$foo = function($x, $y) {
return $x * $y;
};
?>
WTF?!: Let erop dat een closure definiëren vergelijkbaar is met een normaal PHP statement. Daarom moet er een punt-komma volgen na je afsluitende haakje.
Pagina 2
Variabelen importeren in een closure
In tegenstelling tot andere implementaties van closures moet je in PHP expliciet definiëren welke variabelen uit de huidige scope je wilt importeren.
<?php
$foo = "Hello World";
$bar = function($baz) use ($foo) {
echo $foo . ' and ' . $baz;
};
function greet($greet_callback) {
$greet_callback('others');
}
greet($bar);
/* resultaat:
Hello World and others
*/
?>
Variabelen importeren: by reference of niet?
WTF?!: In tegenstelling tot wat je zou verwachten wanneer je veel met Javascript hebt gewerkt importeert PHP de variabelen niet standaard by reference. Dit is wat onverwacht, aangezien het keyword 'global' dit wel doet.
<?php
$my_global_greet = '* Silence *';
$bar_without_reference = function($baz) use ($my_global_greet) {
$my_global_greet = 'Hello ' . $baz;
};
$bar_without_reference('Jelmer');
echo $my_global_greet;
/* Resultaat:
* Silence *
*/
/* Nu met reference (let op het &-teken!) */
$bar_with_reference = function($baz) use (&$my_global_greet) {
$my_global_greet = 'Hello ' . $baz;
};
$bar_with_reference('Jelmer');
echo $my_global_greet;
/* Resultaat:
Hello Jelmer
*/
/* En ter demonstratie (let op het 'global' keyword) */
function bar_the_old_way($baz) {
global $my_global_greet;
$my_global_greet = 'Howdy ' . $baz;
}
bar_the_old_way('Jelmer');
echo $my_global_greet;
/* Resultaat:
Howdy Jelmer
*/
?>
Is dit ongewenst gedrag? Nee. Stel dat de makers ervoor hadden gekozen de weg van 'global' te kiezen, dan was het omslachtiger geweest om een niet-veranderende kopie van een variabele uit de bovenliggende scope te halen. Ook moet je nu expliciet aangeven dat je closure de geïmporteerde variabele mag veranderen, wat het WTF?!-gehalte van je code weer wat naar beneden haalt. Ezelsbruggetje: de schrijfwijze en werking van de argumenten van een functie komen overeen met de schrijfwijze en werking van het importeren van je variabelen bij een closure.
<?php
$foo = "Hello World";
$bar = function($baz) use ($foo) {
echo $foo . ' and ' . $baz;
};
function greet($greet_callback) {
$greet_callback('others');
}
greet($bar);
/* resultaat:
Hello World and others
*/
?>
Variabelen importeren: by reference of niet?
WTF?!: In tegenstelling tot wat je zou verwachten wanneer je veel met Javascript hebt gewerkt importeert PHP de variabelen niet standaard by reference. Dit is wat onverwacht, aangezien het keyword 'global' dit wel doet.
<?php
$my_global_greet = '* Silence *';
$bar_without_reference = function($baz) use ($my_global_greet) {
$my_global_greet = 'Hello ' . $baz;
};
$bar_without_reference('Jelmer');
echo $my_global_greet;
/* Resultaat:
* Silence *
*/
/* Nu met reference (let op het &-teken!) */
$bar_with_reference = function($baz) use (&$my_global_greet) {
$my_global_greet = 'Hello ' . $baz;
};
$bar_with_reference('Jelmer');
echo $my_global_greet;
/* Resultaat:
Hello Jelmer
*/
/* En ter demonstratie (let op het 'global' keyword) */
function bar_the_old_way($baz) {
global $my_global_greet;
$my_global_greet = 'Howdy ' . $baz;
}
bar_the_old_way('Jelmer');
echo $my_global_greet;
/* Resultaat:
Howdy Jelmer
*/
?>
Is dit ongewenst gedrag? Nee. Stel dat de makers ervoor hadden gekozen de weg van 'global' te kiezen, dan was het omslachtiger geweest om een niet-veranderende kopie van een variabele uit de bovenliggende scope te halen. Ook moet je nu expliciet aangeven dat je closure de geïmporteerde variabele mag veranderen, wat het WTF?!-gehalte van je code weer wat naar beneden haalt. Ezelsbruggetje: de schrijfwijze en werking van de argumenten van een functie komen overeen met de schrijfwijze en werking van het importeren van je variabelen bij een closure.
Pagina 3
De scope van 'use'
Let op: keyword 'use' importeert alleen uit de scope waar je je closure definieert. Wil je variabelen importeren uit bijvoorbeeld de global scope, dan zal je het keyword 'global' moeten gebruiken. Tenzij je je closure ook binnen diezelfde global scope definieert.
<?php
$global_a = "Ik ben global_a\n";
function function_a() {
$scope_b = "Ik ben scope_b\n";
$closure_a = function() use ($global_a, $scope_b) {
echo $global_a;
echo $scope_b;
echo "Ik ben closure_a\n";
};
$closure_a();
}
function_a();
/* resultaat:
Ik ben scope_b
Ik ben closure_a
*/
?>
Merk op dat $global_a niet beschikbaar was binnen $closure_a, omdat $global_a niet bestaat binnen de scope van function_a.
<?php
$global_a = "Ik ben global_a\n";
function function_a() {
$scope_b = "Ik ben scope_b\n";
$closure_a = function() use ($global_a, $scope_b) {
echo $global_a;
echo $scope_b;
echo "Ik ben closure_a\n";
};
$closure_a();
}
function_a();
/* resultaat:
Ik ben scope_b
Ik ben closure_a
*/
?>
Merk op dat $global_a niet beschikbaar was binnen $closure_a, omdat $global_a niet bestaat binnen de scope van function_a.
Pagina 4
Closure en objecten
Opmerking: PHP 5.3 Beta heeft geen ondersteuning meer voor $this binnen een closure omdat men het niet eens kon worden over de werking. De hieronder beschreven werking slaat op de alfa versies van PHP 5.3
Hoe zit het met $this binnen closures, en waar zitten de caveats?
Vandaag op het programma:
<?php
class Foo {
protected $greeting;
public $greet;
public function __construct() {
$this->greeting = 'Hello ';
$this->greet = function($subject) {
echo $this->greeting . $subject;
};
}
}
$foo = new Foo();
?>
Foo::$greet is de closure die gebruik maakt van protected Foo::$greeting. Dit mag, want de closure wordt in een object-context gemaakt. $this hoef je niet te importeren, net zoals je dat niet hoeft in __construct. Daarnaast zijn wijzigingen die je doet op $this direct wijzigingen op $foo, zoals verwacht.
Hoe roep je nu $foo->greet aan? Eerste idee wat bij mij opkomt is dan:
<?php
$foo->greet('Jelmer');
?>
WTF?!: Boooh! Dat werkt niet. De klasse Foo heeft geen method genaamd 'greet'. Maar een reference (reference? Zie volgende pagina!) maken werkt wel.
<?php
$greeter = $foo->greet;
$greeter('Jelmer');
/* Resultaat:
Hello Jelmer
*/
?>
* Magic Method __invoke *
Maar wat is een closure nu werkelijk binnen PHP?
<?php
var_dump(function($x, $y) {
return $x * $y;
});
/* resultaat:
object(Closure)#1 (0) {
}
*/
?>
Vergelijk dat met de oude create_function:
<?php
var_dump(create_function('$x, $y', 'return $x * $y;'));
/* resultaat:
string(9) "lambda_1"
*/
?>
Een closure is een object in PHP. Vandaar dat ik een pagina terug ook sprak over een reference. Een closure kopieer je niet (sterker nog, clone werkt niet) maar verwijs je naar zoals standaard is bij objecten in PHP 5.
Maar als Closure een klasse is, kan je hem ook extenden? Nee helaas, Closure is een final class, wat inhoudt dat je hem niet kan uitbreiden. Je kan hem ook niet direct vanuit PHP instantiëren:
<?php
$x = new Closure();
/* Resultaat:
PHP Fatal error: Instantiation of 'Closure' is not allowed
*/
?>
Maaaarrrr... Closure heeft wel een method genaamd '__invoke'! Guess what:
<?php
$foo = function($x, $y) {
return $x * $y;
};
echo $foo->__invoke(2, 3);
/* resultaat:
6
*/
?>
En het wordt leuker!
<?php
class Cube {
protected $depth;
public function __construct($depth) {
$this->depth = $depth;
}
public function __invoke($x, $y) {
return $this->depth * $x * $y;
}
}
$cube = new Cube(4);
echo $cube(4, 4);
/* Resultaat:
64
*/
?>
Dit betekent dat je voortaan instanties van klassen met een __invoke-method direct kan aanroepen of kan opgeven als callback: Dit zal werken zonder gemekker:
<?php
ob_start($cube);
?>
.. het doet alleen niets nuttigs :)
Hoe zit het met $this binnen closures, en waar zitten de caveats?
Vandaag op het programma:
<?php
class Foo {
protected $greeting;
public $greet;
public function __construct() {
$this->greeting = 'Hello ';
$this->greet = function($subject) {
echo $this->greeting . $subject;
};
}
}
$foo = new Foo();
?>
Foo::$greet is de closure die gebruik maakt van protected Foo::$greeting. Dit mag, want de closure wordt in een object-context gemaakt. $this hoef je niet te importeren, net zoals je dat niet hoeft in __construct. Daarnaast zijn wijzigingen die je doet op $this direct wijzigingen op $foo, zoals verwacht.
Hoe roep je nu $foo->greet aan? Eerste idee wat bij mij opkomt is dan:
<?php
$foo->greet('Jelmer');
?>
WTF?!: Boooh! Dat werkt niet. De klasse Foo heeft geen method genaamd 'greet'. Maar een reference (reference? Zie volgende pagina!) maken werkt wel.
<?php
$greeter = $foo->greet;
$greeter('Jelmer');
/* Resultaat:
Hello Jelmer
*/
?>
* Magic Method __invoke *
Maar wat is een closure nu werkelijk binnen PHP?
<?php
var_dump(function($x, $y) {
return $x * $y;
});
/* resultaat:
object(Closure)#1 (0) {
}
*/
?>
Vergelijk dat met de oude create_function:
<?php
var_dump(create_function('$x, $y', 'return $x * $y;'));
/* resultaat:
string(9) "lambda_1"
*/
?>
Een closure is een object in PHP. Vandaar dat ik een pagina terug ook sprak over een reference. Een closure kopieer je niet (sterker nog, clone werkt niet) maar verwijs je naar zoals standaard is bij objecten in PHP 5.
Maar als Closure een klasse is, kan je hem ook extenden? Nee helaas, Closure is een final class, wat inhoudt dat je hem niet kan uitbreiden. Je kan hem ook niet direct vanuit PHP instantiëren:
<?php
$x = new Closure();
/* Resultaat:
PHP Fatal error: Instantiation of 'Closure' is not allowed
*/
?>
Maaaarrrr... Closure heeft wel een method genaamd '__invoke'! Guess what:
<?php
$foo = function($x, $y) {
return $x * $y;
};
echo $foo->__invoke(2, 3);
/* resultaat:
6
*/
?>
En het wordt leuker!
<?php
class Cube {
protected $depth;
public function __construct($depth) {
$this->depth = $depth;
}
public function __invoke($x, $y) {
return $this->depth * $x * $y;
}
}
$cube = new Cube(4);
echo $cube(4, 4);
/* Resultaat:
64
*/
?>
Dit betekent dat je voortaan instanties van klassen met een __invoke-method direct kan aanroepen of kan opgeven als callback: Dit zal werken zonder gemekker:
<?php
ob_start($cube);
?>
.. het doet alleen niets nuttigs :)
Pagina 5
WTF?! - de opsomming
Dingen waar je dus even op moet letten, en die waarschijnlijk de eerste keren nog wel eens fout zullen gaan bij het gebruik van closures:
[li]Variabelen importeren betekent kopiëren net zoals de argumenten die je aan een functie geeft. Wil je ze via een reference, dan moet je dat expliciet aangeven met een &-teken voor de variabelenaam.[/li]
[li]Vergeet de punt-komma niet achter de afsluitende }. Direct aanroepen zoals in Javascript door er direct (args) achter te plaatsen kan niet.[/li]
[li]Is je closure een property van een instantie? Dan kan je hem niet direct aanroepen omdat de Zend Engine dit zal zien als een method-aanroep. Een reference maken of __invoke aanroepen werkt wel[/li]
[li]Wanneer je een closure binnen een object-context aanmaakt, wordt $this standaard voor je geïmporteerd. $this verwijst altijd naar de instantie waar de closure is aangemaakt.[/li]
En nog wat afsluitende Syntax-mogelijkheden:
<?php
function make_multiplier($factor) {
return function($a) use ($factor) {
return $a * $factor;
};
}
/* Werkt wel: resultaat is 12 */
$times_four = make_multiplier(4);
echo $times_four(3);
/* Werkt niet: resultaat is syntax error */
make_multiplier(5)(5);
?>
<?php
class Number {
protected $factor;
public $multiplier;
public function __construct($factor) {
$this->factor = $factor;
$this->multiplier = function($a) {
return $a * $this->factor;
};
}
}
$foo = new Number(4);
/* Werkt wel: resultaat is 12 */
$foo_multiplier = $foo->multiplier;
echo $foo_multiplier(3);
/* Werkt ook: resultaat is 24 */
echo $foo->multiplier->__invoke(6);
/* Werkt niet: undefined method 'Foo::multiplier' */
$foo->multiplier(6);
?>
[li]Variabelen importeren betekent kopiëren net zoals de argumenten die je aan een functie geeft. Wil je ze via een reference, dan moet je dat expliciet aangeven met een &-teken voor de variabelenaam.[/li]
[li]Vergeet de punt-komma niet achter de afsluitende }. Direct aanroepen zoals in Javascript door er direct (args) achter te plaatsen kan niet.[/li]
[li]Is je closure een property van een instantie? Dan kan je hem niet direct aanroepen omdat de Zend Engine dit zal zien als een method-aanroep. Een reference maken of __invoke aanroepen werkt wel[/li]
[li]Wanneer je een closure binnen een object-context aanmaakt, wordt $this standaard voor je geïmporteerd. $this verwijst altijd naar de instantie waar de closure is aangemaakt.[/li]
En nog wat afsluitende Syntax-mogelijkheden:
<?php
function make_multiplier($factor) {
return function($a) use ($factor) {
return $a * $factor;
};
}
/* Werkt wel: resultaat is 12 */
$times_four = make_multiplier(4);
echo $times_four(3);
/* Werkt niet: resultaat is syntax error */
make_multiplier(5)(5);
?>
<?php
class Number {
protected $factor;
public $multiplier;
public function __construct($factor) {
$this->factor = $factor;
$this->multiplier = function($a) {
return $a * $this->factor;
};
}
}
$foo = new Number(4);
/* Werkt wel: resultaat is 12 */
$foo_multiplier = $foo->multiplier;
echo $foo_multiplier(3);
/* Werkt ook: resultaat is 24 */
echo $foo->multiplier->__invoke(6);
/* Werkt niet: undefined method 'Foo::multiplier' */
$foo->multiplier(6);
?>
Pagina 6
Disclaimer
Nog even een waarschuwing achteraf:
Deze tutorial is geschreven ten tijde van de eerste alfa-versie van PHP 5.3. Alle informatie is afkomstig van fansites en weblogs, en uit experimentjes en bladeren door de broncode van PHP. Het zou goed kunnen dat er nog meer functies zijn maar ten tijde van schrijven is er nog geen documentatie beschikbaar van PHP.net zelf.
Ook kan het zijn dat in de definitieve versie van PHP 5.3 het gedrag wordt aangepast of er features worden toegevoegd.
RFC:Closures - wiki.php.net
Deze tutorial is geschreven ten tijde van de eerste alfa-versie van PHP 5.3. Alle informatie is afkomstig van fansites en weblogs, en uit experimentjes en bladeren door de broncode van PHP. Het zou goed kunnen dat er nog meer functies zijn maar ten tijde van schrijven is er nog geen documentatie beschikbaar van PHP.net zelf.
Ook kan het zijn dat in de definitieve versie van PHP 5.3 het gedrag wordt aangepast of er features worden toegevoegd.
RFC:Closures - wiki.php.net
Reacties
0