Objects instantieren met data al in private properties

Overzicht Reageren

Sponsored by: Vacatures door Monsterboard

Pagina: 1 2 volgende »

Erwin H

Erwin H

24/05/2013 15:12:50
Quote Anchor link
Tja, soms kom je op van die vragen.... In dit geval was het van Ozzie overigens in dit topic: 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:
Code (php)
PHP script in nieuw venster Selecteer het PHP script
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
<?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>';
?>

En op mijn scherm krijg ik nu:
Code (php)
PHP script in nieuw venster Selecteer het PHP script
1
2
Name: Erwin
Age: 2

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
 
PHP hulp

PHP hulp

18/11/2024 03:29:44
 
Wouter J

Wouter J

24/05/2013 15:25:56
Quote Anchor link
zou je dit misschien in een tutorial/script willen gieten?
 
Erwin H

Erwin H

24/05/2013 15:27:12
Quote Anchor link
Nee, geen behoefte aan, maar ik laat het iedereen vrij om te gebruiken, dus ook om er een tutorial van te maken.
 
Ozzie PHP

Ozzie PHP

24/05/2013 15:53:43
Quote Anchor link
Wow... je hebt er werk van gemaakt :-)

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? :-)))
 
Stephan G

Stephan G

24/05/2013 15:56:19
Quote Anchor link
Ik zie nog niet de toegevoegde waarde in van het op deze manier instantieren van een class, gaarne daar wat uitleg over? =)

Code (php)
PHP script in nieuw venster Selecteer het PHP script
1
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 );


VS

Code (php)
PHP script in nieuw venster Selecteer het PHP script
1
2
3
$obj = new Test_Class();
$obj->setName('Erwin');
$obj->setAge(2);


Ik zou gaan voor de laatste variant :p.
Gewijzigd op 24/05/2013 15:57:25 door Stephan G
 
Ozzie PHP

Ozzie PHP

24/05/2013 16:00:41
Quote Anchor link
Moet je even 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.
 
Erwin H

Erwin H

24/05/2013 16:02:43
Quote Anchor link
FETCH_CLASS werkt volgens mij anders, maar om eerlijk te zijn kan ik de source code (zie link) niet zover ontrafelen dat ik het echt begrijp.

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:
Code (php)
PHP script in nieuw venster Selecteer het PHP script
1
2
3
$obj = new Test_Class();
$obj->setName('Erwin');
$obj->setAge(2);


Ik zou gaan voor de laatste variant :p.

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 ;-)
 
Stephan G

Stephan G

24/05/2013 16:07:16
Quote Anchor link
Ik snap het principe, maar als je je object wil mappen zonder PDO, zul je toch eerst alle data op moeten halen en die dan moeten verwerken, eens? Dus, als je dan toch al je data aan het fetchen bent, waarom map je het dan niet direct door naar je object terwijl je toch bezig bent, in plaats van eerst alles op te halen, dan een constructie te creëren waarin je zegt welke properties welke waarde moeten krijgen, en dan vervolgens nog je object aan te maken via een fake unserialize constructie?

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
 
Wouter J

Wouter J

24/05/2013 16:08:23
Quote Anchor link
>> Als ik nu het object unserialize, dan krijg ik dus echt een compleet nieuw object? Correct?
Code (php)
PHP script in nieuw venster Selecteer het PHP script
1
2
3
<?php
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
 
Ozzie PHP

Ozzie PHP

24/05/2013 16:15:18
Quote Anchor link
@Erwin: stel dat de class meerdere private properties, bijv. 10 stuks, maar je geeft er maar 2 mee. Werkt het dan ook gewoon?

@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.
 
Erwin H

Erwin H

24/05/2013 16:18:31
Quote Anchor link
Je kan meer en minder waardes opgeven.
Code (php)
PHP script in nieuw venster Selecteer het PHP script
1
2
3
4
5
<?php
$array
= array(
    'name' => 'Erwin'
);
?>

Werkt, maar als ik dan getAge() aanroep krijg ik de default waarde.

Code (php)
PHP script in nieuw venster Selecteer het PHP script
1
2
3
4
5
6
7
<?php
$array
= array(
    'name' => 'Erwin',
    'age' => '2',
    'user' => 'normal'
);
?>

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.
 
Wouter J

Wouter J

24/05/2013 16:24:07
Quote Anchor link
Die assert betekend 'verwacht'. Je verwacht dat het eerste true is. Zodra dat zo is gebeurd er niks. Mocht het echter niet true zijn (maar false) dan wordt er een error gegooid. Hiermee kun je dus snel testen of iets is wat je verwacht dat het is.

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.
 
Ozzie PHP

Ozzie PHP

24/05/2013 16:29:45
Quote Anchor link
@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?

@Wouter: euh... okeej... maar wat is dan de uitkomst? Zijn ze gelijk?
 
Erwin H

Erwin H

24/05/2013 16:34:37
Quote Anchor link
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...
 
Ozzie PHP

Ozzie PHP

24/05/2013 18:21:57
Quote Anchor link
"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.
 
Erwin H

Erwin H

24/05/2013 18:39:16
Quote Anchor link
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.
 
Ozzie PHP

Ozzie PHP

24/05/2013 20:20:40
Quote Anchor link
"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."

Dat komt omdat ie in de class zelf niet als private property is gedefinieerd denk ik.
 

25/05/2013 00:26:12
Quote Anchor link
Even gebruik gemaakt van ReflectionClass & ReflectionProperty en dan is dit eruit gekomen. Er is voor de Instance class wel een static gebruikt en de performance is waarschijnlijk ook niet helemaal top...

Code (php)
PHP script in nieuw venster Selecteer het PHP script
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
<?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();

?>

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.
Gewijzigd op 25/05/2013 01:05:03 door
 
Ozzie PHP

Ozzie PHP

25/05/2013 01:33:36
Quote Anchor link
Huh, Aaron, heb jij nu een andere manier gevonden?

"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?
 

25/05/2013 09:47:34
Quote Anchor link
@Ozzie
Als je het gewoon via setUserId() & getUserId() doet is het dus ongeveer 3x maal sneller. De manier van Erwin heb ik nog niet geprobeerd.
 
Erwin H

Erwin H

25/05/2013 10:46:25
Quote Anchor link
Een nieuwe methode, interessant :-)

Testje gedaan om de performance te meten. Hiervoor heb ik de oorspronkelijke Test_Class iets aangepast:
Code (php)
PHP script in nieuw venster Selecteer het PHP script
1
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'];
  }  
}

?>

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:
Code (php)
PHP script in nieuw venster Selecteer het PHP script
1
2
3
directe instantiering       10.000 runs    0.09 sec
serialized manier           10.000 runs    0.60 sec
reflector class             10.000 runs    0.77 sec
 

Pagina: 1 2 volgende »



Overzicht Reageren

 
 

Om de gebruiksvriendelijkheid van onze website en diensten te optimaliseren maken wij gebruik van cookies. Deze cookies gebruiken wij voor functionaliteiten, analytische gegevens en marketing doeleinden. U vindt meer informatie in onze privacy statement.