Objects instantieren met data al in private properties
http://www.phphulp.nl/php/forum/topic/welke-fetch-mode/90810/
Vraag (mijn vertaling in elk geval): hoe kan je een instantie maken van een object waarin de private properties al een waarde hebben, zonder dat de setters worden aangeroepen?
Uhm, mijn eerste ingeving zou zijn: dat kan niet. Echter, de PDO fetch methode FETCH_CLASS zorgt ervoor dat dit wel gebeurt. Het moet dus kunnen. Inmiddels ben ik wel achter de broncode van die methode gekomen, maar daar werd ik niet veel wijzer van: https://github.com/php/php-src/blob/master/ext/pdo/pdo_stmt.c#L1329
Ergens anders kwam ik nog een hint tegen: via serialize/unserialize kan het wel.
Hmm, zou moeten kunnen. Wat serialize doet is een string maken van een object en unserialize maakt van die string weer een object. Daarin zitten ook de private properties (en hun waarde) en dus zou je zelf zo'n string moeten kunnen bouwen en dan met unserialize dat object moeten hebben.
Proberen, paar keer vloeken omdat het niet werkte, hex en unhex en een paar 0 bits erin en hoppa:
En op mijn scherm krijg ik nu:
Een object aangemaakt met data in private properties, zonder dat de class de benodigde setters ervoor heeft.
Ha, vanavond feest!
Tja, soms kom je op van die vragen.... In dit geval was het van Ozzie overigens in dit topic: Vraag (mijn vertaling in elk geval): hoe kan je een instantie maken van een object waarin de private properties al een waarde hebben, zonder dat de setters worden aangeroepen?
Uhm, mijn eerste ingeving zou zijn: dat kan niet. Echter, de PDO fetch methode FETCH_CLASS zorgt ervoor dat dit wel gebeurt. Het moet dus kunnen. Inmiddels ben ik wel achter de broncode van die methode gekomen, maar daar werd ik niet veel wijzer van: https://github.com/php/php-src/blob/master/ext/pdo/pdo_stmt.c#L1329
Ergens anders kwam ik nog een hint tegen: via serialize/unserialize kan het wel.
Hmm, zou moeten kunnen. Wat serialize doet is een string maken van een object en unserialize maakt van die string weer een object. Daarin zitten ook de private properties (en hun waarde) en dus zou je zelf zo'n string moeten kunnen bouwen en dan met unserialize dat object moeten hebben.
Proberen, paar keer vloeken omdat het niet werkte, hex en unhex en een paar 0 bits erin en hoppa:
Code (php)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<?php
//Class to be instantiated
class Test_Class{
private $name = 'Ozzie';
private $age = '1';
public function getName(){
return $this->name;
}
public function getAge(){
return $this->age;
}
}
//variables to instantiate with
$classname = 'Test_Class';
$array = array(
'name' => 'Erwin',
'age' => '2'
);
//build the serialized string
$string = 'O:'.strlen($classname).':"'.$classname.'":'.count( $array ).':{';
foreach( $array as $key => $value ){
//s:16:"Test_Classname";
$string .= 's:'.strlen(chr(0).$classname.chr(0).$key).':"'.chr(0).$classname.chr(0).$key.'";';
//s:5:"Erwin";
$string .= 's:'.strlen($value).':"'.$value.'";';
}
$string .= '}';
//unserialize and test
$obj = unserialize( $string );
echo 'Name: '.$obj->getName().'<br>';
echo 'Age: '.$obj->getAge().'<br>';
?>
//Class to be instantiated
class Test_Class{
private $name = 'Ozzie';
private $age = '1';
public function getName(){
return $this->name;
}
public function getAge(){
return $this->age;
}
}
//variables to instantiate with
$classname = 'Test_Class';
$array = array(
'name' => 'Erwin',
'age' => '2'
);
//build the serialized string
$string = 'O:'.strlen($classname).':"'.$classname.'":'.count( $array ).':{';
foreach( $array as $key => $value ){
//s:16:"Test_Classname";
$string .= 's:'.strlen(chr(0).$classname.chr(0).$key).':"'.chr(0).$classname.chr(0).$key.'";';
//s:5:"Erwin";
$string .= 's:'.strlen($value).':"'.$value.'";';
}
$string .= '}';
//unserialize and test
$obj = unserialize( $string );
echo 'Name: '.$obj->getName().'<br>';
echo 'Age: '.$obj->getAge().'<br>';
?>
En op mijn scherm krijg ik nu:
Een object aangemaakt met data in private properties, zonder dat de class de benodigde setters ervoor heeft.
Ha, vanavond feest!
Gewijzigd op 24/05/2013 15:14:31 door Erwin H
zou je dit misschien in een tutorial/script willen gieten?
Nee, geen behoefte aan, maar ik laat het iedereen vrij om te gebruiken, dus ook om er een tutorial van te maken.
Als ik het goed begrijp "fake" je dus eigenlijk een serialized string waarin je een object opslaat, en in die string verwerk je de properties. Correct? Haha, hoe verzin je het :-) Werkt dat fetch_class ook (ongeveer) op deze manier?
- Als ik nu het object unserialize, dan krijg ik dus echt een compleet nieuw object? Correct?
- Waar staat chr(0) precies voor?
- Maar als ik het goed begrijp heb je door dit te gebruiken dus fetch_class helemaal niet meer nodig? :-)))
Code (php)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//variables to instantiate with
$classname = 'Test_Class';
$array = array(
'name' => 'Erwin',
'age' => '2'
);
//build the serialized string
$string = 'O:'.strlen($classname).':"'.$classname.'":'.count( $array ).':{';
foreach( $array as $key => $value ){
//s:16:"Test_Classname";
$string .= 's:'.strlen(chr(0).$classname.chr(0).$key).':"'.chr(0).$classname.chr(0).$key.'";';
//s:5:"Erwin";
$string .= 's:'.strlen($value).':"'.$value.'";';
}
$string .= '}';
//unserialize and test
$obj = unserialize( $string );
$classname = 'Test_Class';
$array = array(
'name' => 'Erwin',
'age' => '2'
);
//build the serialized string
$string = 'O:'.strlen($classname).':"'.$classname.'":'.count( $array ).':{';
foreach( $array as $key => $value ){
//s:16:"Test_Classname";
$string .= 's:'.strlen(chr(0).$classname.chr(0).$key).':"'.chr(0).$classname.chr(0).$key.'";';
//s:5:"Erwin";
$string .= 's:'.strlen($value).':"'.$value.'";';
}
$string .= '}';
//unserialize and test
$obj = unserialize( $string );
VS
Ik zou gaan voor de laatste variant :p.
Gewijzigd op 24/05/2013 15:57:25 door Stephan G
dit topic lezen.
Het gaat erom dat PDO een makkelijke manier heeft om data in een object te stoppen. In plaats van dat je setters moet gebruiken regelt PDO dat. Als je 10 producten uit je database haalt, dan hoef je niet eerst 10 rijen in te laden en die vervolgens via een setter stuk voor stuk te instantiëren. Dat regelt PDO dan voor je. Echter, andere database interfaces ondersteunen dit niet, maar met de oplossing van Erwin kan het dus wel.
Stel je query zou dit zijn:
$query = "SELECT id, name, description FROM products LIMIT 10";
En je stelt de juiste fetch mode in, dan krijg je dus direct 10 Product objecten terug met de juiste properties.
Moet je even Het gaat erom dat PDO een makkelijke manier heeft om data in een object te stoppen. In plaats van dat je setters moet gebruiken regelt PDO dat. Als je 10 producten uit je database haalt, dan hoef je niet eerst 10 rijen in te laden en die vervolgens via een setter stuk voor stuk te instantiëren. Dat regelt PDO dan voor je. Echter, andere database interfaces ondersteunen dit niet, maar met de oplossing van Erwin kan het dus wel.
Stel je query zou dit zijn:
$query = "SELECT id, name, description FROM products LIMIT 10";
En je stelt de juiste fetch mode in, dan krijg je dus direct 10 Product objecten terug met de juiste properties.
Als je nu unserialize doet dat maak je in feite een object aan met de eigenschappen zoals in de string zijn meegegeven. Je maakt dus een nieuw object (of misschien beter, je converteert het object van string vorm naar executable code vorm).
De functie chr() maakt van een ascii code een string (met maar 1 karakter). De ascii code 0 staat voor in feite niets, maar neemt wel ruimte in. Zonder die nul byte krijg je een foutmelding. Ik vermoed dat die nul byte wordt gezien als een delimiter.
Je zou dit kunnen gebruiken in plaats van FETCH_CLASS. Uiteraard kan ik niet garanderen dat dit altijd perfect werkt en ook sneller is, dat mag jezelf in jouw applicatie bepalen. In elk geval zou je dit kunnen gebruiken voor die database interfaces doe FETCH_CLASS niet ondersteunen.
Toevoeging op 24/05/2013 16:04:05:
Stephan G op 24/05/2013 15:56:19:
Afgezien van het feit dat de vraag heel specifiek was, gaat jouw bovenstaande code niet werken. Merk op dat Test_Class geen setters voor die properties heeft. Ik zou dus niet gaan voor die variant in dit geval ;-)
Inventief vind ik het zeker, maar ik zou het denk ik niet gebruiken. Of iemand moet me kunnen vertellen dat ik 't niet begrijp zoals bedoeld :p.
Gewijzigd op 24/05/2013 16:07:32 door Stephan G
Code (php)
1
2
3
2
3
<?php
assert(deserialize('O:8:StdClass:0:{};') !== deserialize('O:8:StdClass:0:{};'), 'Serialize does not create new instances');
?>
assert(deserialize('O:8:StdClass:0:{};') !== deserialize('O:8:StdClass:0:{};'), 'Serialize does not create new instances');
?>
>> - Maar als ik het goed begrijp heb je door dit te gebruiken dus fetch_class helemaal niet meer nodig? :-)))
Dit is je eigen fetch_class
@Stephan: ik denk dat je het niet begrijpt. Je kunt waarden uit een database direct als property instellen zonder dat je per se iets hoeft te setten met specifieke setters.
@Wouter: dat assert begrijp ik niet zo. Wat wil je precies zeggen? En wat bedoel je met "Dit is je eigen fetch_class"? Mijn vraag was... als ik de code van Erwin gebruik en ik haal bijv. 10 producten op, of ik dan dus 10 nieuwe instanties van de product class terugkrijg.
Werkt, maar als ik dan getAge() aanroep krijg ik de default waarde.
Werkt en dan wordt er zelfs een nieuw private property '$user' aangemaakt. Die kan je alleen niet uitlezen, maar wordt wel getoond met een var_dump.
In dit geval deserialize ik 2x een string die een lege StdClass voorstelt. Dit is de string die je terug krijgt als je de code van Erwin op een lege StdClass gebruikt. Met === kijken we of dit dezelfde instances zijn of niet. In dit geval gebruiken we !==, zodra het dus niet dezelfde instances zijn krijgen we true en wordt de code uitgevoerd. Als het wel dezelfde instances zijn wordt het false en zal assert een error gooien.
@Wouter: euh... okeej... maar wat is dan de uitkomst? Zijn ze gelijk?
Stephan G op 24/05/2013 15:56:19:
Ik zie nog niet de toegevoegde waarde in van het op deze manier instantieren van een class, gaarne daar wat uitleg over? =)
Toch nog even wat uitleg.
De situatie waarin gestelde methode handig is is als je niet weet wat voor data je opvraagt en niet weet wat voor object je het in moet steken. Als je namelijk de class niet kent, kan je ook de setters niet aanroepen. Dus in feite kan je dan geen algemene functie schrijven die wat voor collectie aan data dan ook in wat voor object dan ook stopt.
Wil je dus een algemeen werkende database class hebben (of van mijn part een xml of csv uitlees object) die de data uitleest en in een object stopt dan moet je of elke keer al die code gaan zitten tikken om de specifieke setters aan te roepen (en dus heb je geen algemeen werkende class meer), of je moet de hele array in 1 keer erin kunnen zetten. Met deze methode kan je ongeacht welke array met data je krijgt en ongeacht welk object gevuld moet worden deze taak volbrengen. Met andere woorden, ik heb nu 1 functie die ik de rest van mijn leven kan gebruiken voor elke applicatie waarvan ik nog niet weet dat ik die ga schrijven. Scheelt me weer veel tijd (en jou nu ook, want jij kan het nu ook gebruiken).
Toevoeging op 24/05/2013 16:38:21:
Ozzie PHP op 24/05/2013 16:29:45:
@Erwin: zou je het ook nog zo kunnen maken dat user dan als public property wordt ingesteld (omdat de private property $user niet bestaat)? Of is dat onmogelijk?
Theoretisch wel, alleen zou ik dan ten tijde van het bouwen van de serialize string al moeten weten welke properties public moeten zijn. Dat weet ik niet, want ik heb geen zicht op het object (dat zou wel kunnen via een reflector class, maar dan ga je wel ver).
Een ander probleem is dat ik het wel heb geprobeerd via een magic getter, maar dat lukt me niet, omdat ik die runtime gegenereerde property niet kon vinden. Zelfs als ik in de var_dump zie dat $user als property bestaat, dan nog krijg ik via isset( $this->user ) false terug. Daar moet dus nog iets op gevonden worden...
Dat komt misschien omdat ie als private wordt geset in de serialize string, terwijl ie niet als private in de class voorkomt???
Dan zou je idd met een reflection class moeten werken. Op zich ook niet echt een probleem. Het nadeel is alleen dat dat de boel wat zal vertragen. Reflection classes zijn niet echt heel snel.
Ozzie PHP op 24/05/2013 18:21:57:
Dat komt misschien omdat ie als private wordt geset in de serialize string, terwijl ie niet als private in de class voorkomt???
Geen idee waar het probleem zit eigenlijk. Als ik een onbestaand property in de string meegeef als een private property dan kan ik die ook niet via een getter benaderen. Als ik het als public property meegeef dan kan ik het direct van buiten benaderen en ook via een getter.
Dat komt omdat ie in de class zelf niet als private property is gedefinieerd denk ik.
Code (php)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
<?php
class Instance {
/**
* Create a new instance of the object and add some properties even
* when they are private or protected.
*
* @param string $class,
* @param array $properties,
* @return bool | object
*/
public static function create($class, array $properties = array()) {
/**
* Check if the class exists. When the class could not been loaded
* return false.
*/
if(class_exists($class, true) === false) {
return false;
}
/**
* Go on. Make the class and add all the properties the user gave
* in the array.
*/
$instance = new $class();
$reflection = new ReflectionClass($class);
foreach((array) $reflection->getProperties() as $i => $reflectionProperty) {
/**
* Get the property name with the getName() method in the
* ReflectionProperty class.
*/
$property = $reflectionProperty->getName();
/**
* When the property has been set in the $properties array we'll set it
* into the object.
*/
if(isset($properties[$property]) === true) {
$reflectionProperty = $reflection->getProperty($property);
$reflectionProperty->setAccessible(true);
$reflectionProperty->setValue($instance, $properties[$property]);
}
}
return $instance;
}
}
class User {
/**
* The identifier for the user.
*
* @param int $userId
*/
private $userId = 'ForNowAString';
/**
* Add the user id to this class.
*
* @param int $userId,
* @return void
*/
public function setUserId($userId) {
$this->userId = $userId;
}
/**
* Return the identifier for the user.
*
* @return int $userId
*/
public function getUserId() {
return (int) $this->userId;
}
}
$user = Instance::create('User', array('userId' => 1));
echo $user->getUserId();
?>
class Instance {
/**
* Create a new instance of the object and add some properties even
* when they are private or protected.
*
* @param string $class,
* @param array $properties,
* @return bool | object
*/
public static function create($class, array $properties = array()) {
/**
* Check if the class exists. When the class could not been loaded
* return false.
*/
if(class_exists($class, true) === false) {
return false;
}
/**
* Go on. Make the class and add all the properties the user gave
* in the array.
*/
$instance = new $class();
$reflection = new ReflectionClass($class);
foreach((array) $reflection->getProperties() as $i => $reflectionProperty) {
/**
* Get the property name with the getName() method in the
* ReflectionProperty class.
*/
$property = $reflectionProperty->getName();
/**
* When the property has been set in the $properties array we'll set it
* into the object.
*/
if(isset($properties[$property]) === true) {
$reflectionProperty = $reflection->getProperty($property);
$reflectionProperty->setAccessible(true);
$reflectionProperty->setValue($instance, $properties[$property]);
}
}
return $instance;
}
}
class User {
/**
* The identifier for the user.
*
* @param int $userId
*/
private $userId = 'ForNowAString';
/**
* Add the user id to this class.
*
* @param int $userId,
* @return void
*/
public function setUserId($userId) {
$this->userId = $userId;
}
/**
* Return the identifier for the user.
*
* @return int $userId
*/
public function getUserId() {
return (int) $this->userId;
}
}
$user = Instance::create('User', array('userId' => 1));
echo $user->getUserId();
?>
Even een snelle benchmark gedaan en met 1.000 iteraties zonder de Instance class duurt het ongeveer 0.006 seconden en met de Instance class ongeveer 0.018 seconden. Een serieus performance verschil.
"zonder de Instance class duurt het ongeveer 0.006 seconden"
Bedoel je dan dat je het via de setters in de User class set, of op de manier van Erwin?
Als je het gewoon via setUserId() & getUserId() doet is het dus ongeveer 3x maal sneller. De manier van Erwin heb ik nog niet geprobeerd.
Testje gedaan om de performance te meten. Hiervoor heb ik de oorspronkelijke Test_Class iets aangepast:
Code (php)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
class Test_Class{
private $name = 'Ozzie';
private $age = '1';
public function getName(){
return $this->name;
}
public function getAge(){
return $this->age;
}
public function __construct( array $data ){
if ( isset( $data['name'] ) ) $this->name = $data['name'];
if ( isset( $data['age'] ) ) $this->age = $data['age'];
}
}
?>
class Test_Class{
private $name = 'Ozzie';
private $age = '1';
public function getName(){
return $this->name;
}
public function getAge(){
return $this->age;
}
public function __construct( array $data ){
if ( isset( $data['name'] ) ) $this->name = $data['name'];
if ( isset( $data['age'] ) ) $this->age = $data['age'];
}
}
?>
En dit gaf direct een probleem met de methode van Aaron. Als ik die nu aanroep krijg ik de melding dat de constructor een array als parameter nodig heeft.... Hmm, dat is natuurlijk niet handig, want ik wil eigenlijk niet dat de database class zometeen zich nog zorgen moet gaan maken over parameters in de constructor natuurlijk. Voor nu is het opgelost door de parameter als default een lege array te geven dan krijg ik geen problemen meer (en worden de private properties correct geplaatst overigens).
Vervolgens een aantal keer de drie methodes gedraaid en daar komt grofweg dit uit: