59

Is there any way to create all instance properties dynamically? For example, I would like to be able to generate all attributes in the constructor and still be able to access them after the class is instantiated like this: $object->property. Note that I want to access the properties separately, and not using an array; here's an example of what I don't want:

class Thing {
    public $properties;
    function __construct(array $props=array()) {
        $this->properties = $props;
    }
}
$foo = new Thing(array('bar' => 'baz');
# I don't want to have to do this:
$foo->properties['bar'];
# I want to do this:
//$foo->bar;

To be more specific, when I'm dealing with classes that have a large number of properties, I would like to be able to select all columns in a database (which represent the properties) and create instance properties from them. Each column value should be stored in a separate instance property.

outis
  • 75,655
  • 22
  • 151
  • 221
Brayn
  • 1,766
  • 10
  • 27
  • 33
  • 1
    The array and the __set and __get methods ought to be sufficient for normal purposes. Do you have any special concern to stick a "one property=one variable" solution? BTW, the principle of encapsulation dictates no other object should know if the variable is a real one or a value in an array? Not even objects made by extending this base. – Csaba Kétszeri May 06 '09 at 15:31
  • http://php.net/manual/en/language.oop5.overloading.php – Keyne Viana Nov 08 '11 at 18:22

12 Answers12

62

Sort of. There are magic methods that allow you to hook your own code up to implement class behavior at runtime:

class foo {
  public function __get($name) {
    return('dynamic!');
  }
  public function __set($name, $value) {
    $this->internalData[$name] = $value;
  }
}

That's an example for dynamic getter and setter methods, it allows you to execute behavior whenever an object property is accessed. For example

print(new foo()->someProperty);

would print, in this case, "dynamic!" and you could also assign a value to an arbitrarily named property in which case the __set() method is silently invoked. The __call($name, $params) method does the same for object method calls. Very useful in special cases. But most of the time, you'll get by with:

class foo {
  public function __construct() {
    foreach(getSomeDataArray() as $k => $value)
      $this->{$k} = $value;
  }
}

...because mostly, all you need is to dump the content of an array into correspondingly named class fields once, or at least at very explicit points in the execution path. So, unless you really need dynamic behavior, use that last example to fill your objects with data.

This is called overloading http://php.net/manual/en/language.oop5.overloading.php

outis
  • 75,655
  • 22
  • 151
  • 221
Udo
  • 1,784
  • 11
  • 9
  • This is what I need but will these attributes be private or public? Or can I make them private like so: private $this->{$k} => $value; ? – Brayn May 06 '09 at 15:24
  • 5
    They'll be public, to my knowledge it's not possible to make them private or protected at runtime. For "private" visibility, you could declare a private array-type field and then fill that. Same goes for "protected". It's best to keep things as simple as possible, so you could just introduce a private array called $this->ds where fields inside can then be addressed by statements like $this->ds['fieldname']. Or if you want to be really fancy, you'll implement a mini class that essentially wraps the functionality of an array, so you can do something like $this->myDatasetObject->fieldname. – Udo May 06 '09 at 16:13
  • 1
    You can overload any object in PHP5, without __get and __set, but the properties are always public. If you need to protect things, you need to declare them explicitly. – James Socol May 06 '09 at 19:56
  • 2
    I believe ReflectionMethod::setAccessible can help you change the private access of a method – B T Feb 13 '11 at 11:18
23

It depends exactly what you want. Can you modify the class dynamically? Not really. But can you create object properties dynamically, as in one particular instance of that class? Yes.

class Test
{
    public function __construct($x)
    {
        $this->{$x} = "dynamic";
    }
}

$a = new Test("bar");
print $a->bar;

Outputs:

dynamic

So an object property named "bar" was created dynamically in the constructor.

Chad Birch
  • 73,098
  • 23
  • 151
  • 149
7

You can use an instance variable to act as a holder for arbitrary values and then use the __get magic method to retrieve them as regular properties:

class My_Class
{
    private $_properties = array();

    public function __construct(Array $hash)
    {
         $this->_properties = $hash;
    }

    public function __get($name)
    {
         if (array_key_exists($name, $this->_properties)) {
             return $this->_properties[$name];
         }
         return null;
    }
}
Carlton Gibson
  • 7,278
  • 2
  • 36
  • 46
  • I personally like your solution as you have more control over how the variables are used in terms on scope and what have you, but he just posted this. EDIT: Sorry, forgot to mention: Without using an array to hold the properties, each property in a separate 'variable'. – The Pixel Developer May 06 '09 at 15:16
  • Thanks for the comment - I think it's the natural way to go about the problem. I'm afraid I don't understand exactly what Brayn means by his edit — and being new I don't have enough reputation points to post a comment to ask. Oh well... – Carlton Gibson May 06 '09 at 15:19
  • Hi, I really like this answer. I tried editing it to make a fix but Stackoverflow rejects the change as its a single character edit: `return this->_properties[$name];` should probably be `return $this->_properties[$name];` – Iain May 16 '12 at 01:35
7

Yes, you can.

class test
{
    public function __construct()
    {
        $arr = array
        (
            'column1',
            'column2',
            'column3'
        );

        foreach ($arr as $key => $value)
        {
            $this->$value = '';
        }   
    }

    public function __set($key, $value)
    {
        $this->$key = $value;
    }

    public function __get($value)
    {
        return 'This is __get magic '.$value;
    }
}

$test = new test;

// Results from our constructor test.
var_dump($test);

// Using __set
$test->new = 'variable';
var_dump($test);

// Using __get
print $test->hello;

Output

object(test)#1 (3) {
  ["column1"]=>
  string(0) ""
  ["column2"]=>
  string(0) ""
  ["column3"]=>
  string(0) ""
}
object(test)#1 (4) {
  ["column1"]=>
  string(0) ""
  ["column2"]=>
  string(0) ""
  ["column3"]=>
  string(0) ""
  ["new"]=>
  string(8) "variable"
}
This is __get magic hello

This code will set dynamic properties in the constructor which can then be accessed with $this->column. It's also good practice to use the __get and __set magic methods to deal with properties that are not defined within the class. More information them can be found here.

http://www.tuxradar.com/practicalphp/6/14/2

http://www.tuxradar.com/practicalphp/6/14/3

The Pixel Developer
  • 13,282
  • 10
  • 43
  • 60
6

Why is every example so complicated?

<?php namespace example;

error_reporting(E_ALL | E_STRICT); 

class Foo
{
    // class completely empty
}

$testcase = new Foo();
$testcase->example = 'Dynamic property';
echo $testcase->example;
Adrian Cumpanasu
  • 1,028
  • 1
  • 10
  • 24
srcspider
  • 10,977
  • 5
  • 40
  • 35
  • @AdiCumpanasu \error_reporting is actually just saying "global namespace error_reporting function" it's not an escape character; well since the question was asked I've found writing the correct version in php to be more annoying (particularly due to the confusion it causes to people too used to non-namespace php) then writing the "php can figure it out" version so revision is fine. – srcspider Jul 21 '14 at 08:23
4

Here is simple function to populate object members without making class members public. It also leaves constructor for your own usage, creating new instance of object without invoking constructor! So, your domain object doesn't depend on database!


/**
 * Create new instance of a specified class and populate it with given data.
 *
 * @param string $className
 * @param array $data  e.g. array(columnName => value, ..)
 * @param array $mappings  Map column name to class field name, e.g. array(columnName => fieldName)
 * @return object  Populated instance of $className
 */
function createEntity($className, array $data, $mappings = array())
{
    $reflClass = new ReflectionClass($className);
    // Creates a new instance of a given class, without invoking the constructor.
    $entity = unserialize(sprintf('O:%d:"%s":0:{}', strlen($className), $className));
    foreach ($data as $column => $value)
    {
        // translate column name to an entity field name
        $field = isset($mappings[$column]) ? $mappings[$column] : $column;
        if ($reflClass->hasProperty($field))
        {
            $reflProp = $reflClass->getProperty($field);
            $reflProp->setAccessible(true);
            $reflProp->setValue($entity, $value);
        }
    }
    return $entity;
}

/******** And here is example ********/

/**
 * Your domain class without any database specific code!
 */
class Employee
{
    // Class members are not accessible for outside world
    protected $id;
    protected $name;
    protected $email;

    // Constructor will not be called by createEntity, it yours!
    public function  __construct($name, $email)
    {
        $this->name = $name;
        $this->emai = $email;
    }

    public function getId()
    {
        return $this->id;
    }

    public function getName()
    {
        return $this->name;
    }

    public function getEmail()
    {
        return $this->email;
    }
}


$row = array('employee_id' => '1', 'name' => 'John Galt', 'email' => 'john.galt@whoisjohngalt.com');
$mappings = array('employee_id' => 'id'); // Employee has id field, so we add translation for it
$john = createEntity('Employee', $row, $mappings);

print $john->getName(); // John Galt
print $john->getEmail(); // john.galt@whoisjohngalt.com
//...

P.S. Retrieving data from object is similar, e.g. use $reflProp->setValue($entity, $value); P.P.S. This function is heavily inspired by Doctrine2 ORM which is awesome!

Sergiy Sokolenko
  • 5,967
  • 35
  • 37
2
class DataStore // Automatically extends stdClass
{
  public function __construct($Data) // $Data can be array or stdClass
  {
    foreach($Data AS $key => $value)  
    {
        $this->$key = $value;    
    }  
  }
}

$arr = array('year_start' => 1995, 'year_end' => 2003);
$ds = new DataStore($arr);

$gap = $ds->year_end - $ds->year_start;
echo "Year gap = " . $gap; // Outputs 8
Anthony
  • 47
  • 1
  • 1
    `class DataStore {} var_export(is_subclass_of('DataStore', 'StdClass'));` is `false`! Classes do not automatically extend StdClass in PHP. – BenMorel Aug 10 '11 at 15:47
  • Sir, I felt sure I read it somewhere in php.net! Aside, this code does output "123" . . . [code] class foo { public $a=1; public function __construct() { $this->b = 2; } } $bar = new foo; $bar->c = 3; echo $bar->a.$bar->b.$bar->c;[code] – Anthony Aug 19 '11 at 09:12
  • @Anthony classes do not automatically extend stdClass in PHP, however PHP allows adding dynamic properties to an existing object (as of version 8), it might change in future – Faizan Akram Dar Aug 31 '21 at 07:13
1

Extend stdClass.

class MyClass extends stdClass
{
    public function __construct()
    {
        $this->prop=1;
    }
}

I hope this is what you need.

Máthé Endre-Botond
  • 4,826
  • 2
  • 29
  • 48
Anthony
  • 47
  • 1
1

You can:

$variable = 'foo';
$this->$variable = 'bar';

Would set the attribute foo of the object it's called on to bar.

You can also use functions:

$this->{strtolower('FOO')} = 'bar';

This would also set foo (not FOO) to bar.

Koraktor
  • 41,357
  • 10
  • 69
  • 99
0

After reading @Udo 's answer. I've come up with the following pattern, that doesn't bloat a class instance with what-ever items that is in your constructor array argument but still let you type less and easily add new properties to the class.

class DBModelConfig
{
    public $host;
    public $username;
    public $password;
    public $db;
    public $port = '3306';
    public $charset = 'utf8';
    public $collation = 'utf8_unicode_ci';

    public function __construct($config)
    {
        foreach ($config as $key => $value) {
            if (property_exists($this, $key)) {
                $this->{$key} = $value;
            }
        }
    }
}

Then you can pass arrays like:

[
    'host'      => 'localhost',
    'driver'    => 'mysql',
    'username'  => 'myuser',
    'password'  => '1234',
    'charset'   => 'utf8',
    'collation' => 'utf8_unicode_ci',
    'db'        => 'key not used in receiving class'
]
Community
  • 1
  • 1
dotnetCarpenter
  • 10,019
  • 6
  • 32
  • 54
0

This is really complicated way to handle this kind of rapid development. I like answers and magic methods but in my opinion it is better to use code generators like CodeSmith.

I have made template that connect to database, read all columns and their data types and generate whole class accordingly.

This way I have error free (no typos) readable code. And if your database model changes run generator again... it works for me.

zidane
  • 642
  • 1
  • 7
  • 19
0

If you really really must do it, the best way is to overload an ArrayObject, that allows to maintain iteration support (foreach) that will still loop through all your properties.

I note that you said "without using an array", and I just want to assure you that that while technically an array is being used in the background, you NEVER HAVE TO SEE IT. You access all properties via ->properyname or foreach ($class in $name => $value).

Here is a sample I was working on yesterday, note this is also STRONGLY TYPED. So properties that are marked "integer" will throw an error if you try and supply a "string".

You can remove that of course.

There is also an AddProperty() member function, although it is not demonstrated in the example. That will allow you to add properties later.

Sample usage:

    $Action = new StronglyTypedDynamicObject("Action",
            new StrongProperty("Player", "ActionPlayer"),   // ActionPlayer
            new StrongProperty("pos", "integer"),
            new StrongProperty("type", "integer"),
            new StrongProperty("amount", "double"),
            new StrongProperty("toCall", "double"));

    $ActionPlayer = new StronglyTypedDynamicObject("ActionPlayer",
            new StrongProperty("Seat", "integer"),
            new StrongProperty("BankRoll", "double"),
            new StrongProperty("Name", "string"));

    $ActionPlayer->Seat = 1;
    $ActionPlayer->Name = "Doctor Phil";

    $Action->pos = 2;
    $Action->type = 1;
    $Action->amount = 7.0;
    $Action->Player = $ActionPlayer;

    $newAction = $Action->factory();
    $newAction->pos = 4;

    print_r($Action);
    print_r($newAction);


    class StrongProperty {
            var $value;
            var $type;
            function __construct($name, $type) {
                    $this->name = $name;
                    $this->type = $type;
            }

    }

    class StronglyTypedDynamicObject extends ModifiedStrictArrayObject {

            static $basic_types = array(
                    "boolean",
                    "integer",
                    "double",
                    "string",
                    "array",
                    "object",
                    "resource",
            );

            var $properties = array(
                    "__objectName" => "string"
            );

            function __construct($objectName /*, [ new StrongProperty("name", "string"), [ new StrongProperty("name", "string"), [ ... ]]] */) {
                    $this->__objectName = $objectName;
                    $args = func_get_args();
                    array_shift($args);
                    foreach ($args as $arg) {
                            if ($arg instanceof StrongProperty) {
                                    $this->AddProperty($arg->name, $arg->type);
                            } else {
                                    throw new Exception("Invalid Argument");
                            }
                    }
            }

            function factory() {
                    $new = clone $this;
                    foreach ($new as $key => $value) {
                            if ($key != "__objectName") {
                                    unset($new[$key]);
                            }
                    }

                    // $new->__objectName = $this->__objectName;
                    return $new;
            }

            function AddProperty($name, $type) {
                    $this->properties[$name] = $type;
                    return;

                    if (in_array($short_type, self::$basic_types)) {
                            $this->properties[$name] = $type;
                    } else {
                            throw new Exception("Invalid Type: $type");
                    }
            }

            public function __set($name, $value) {
                    self::sdprintf("%s(%s)\n", __FUNCTION__, $name);
                    $this->check($name, $value);
                    $this->offsetSet($name, $value);
            }

            public function __get($name) {
                    self::sdprintf("%s(%s)\n", __FUNCTION__, $name);
                    $this->check($name);
                    return $this->offsetGet($name);
            }

            protected function check($name, $value = "r4nd0m") {
                    if (!array_key_exists($name, $this->properties)) {
                            throw new Exception("Attempt to access non-existent property '$name'");
                    }

                    $value__objectName = "";
                    if ($value != "r4nd0m") {
                            if ($value instanceof StronglyTypedDynamicObject) {
                                    $value__objectName = $value->__objectName;
                            }
                            if (gettype($value) != $this->properties[$name] && $value__objectName != $this->properties[$name]) { 
                                    throw new Exception("Attempt to set {$name} ({$this->properties[$name]}) with type " . gettype($value) . ".$value__objectName");
                            }
                    }
            }
    }

    class ModifiedStrictArrayObject extends ArrayObject {
            static $debugLevel = 0;

            /* Some example properties */

            static public function StaticDebug($message) {
                    if (static::$debugLevel > 1) {
                            fprintf(STDERR, "%s\n", trim($message));
                    }
            }

            static public function sdprintf() {
                    $args = func_get_args();
                    $string = call_user_func_array("sprintf", $args);
                    self::StaticDebug("D            " . trim($string));
            }

            protected function check($name) {
                    if (!array_key_exists($name, $this->properties)) {
                            throw new Exception("Attempt to access non-existent property '$name'");
                    }
            }

            //static public function sget($name, $default = NULL) {
            /******/ public function get ($name, $default = NULL) {
                    self::sdprintf("%s(%s)\n", __FUNCTION__, $name);
                    $this->check($name);
                    if (array_key_exists($name, $this->storage)) {
                            return $this->storage[$name];
                    }
                    return $default;
            }

            public function offsetGet($name) { 
                    self::sdprintf("%s(%s)\n", __FUNCTION__, implode(",", func_get_args()));
                    $this->check($name);
                    return call_user_func_array(array(parent, __FUNCTION__), func_get_args());
            }
            public function offsetSet($name, $value) { 
                    self::sdprintf("%s(%s)\n", __FUNCTION__, implode(",", func_get_args()));
                    $this->check($name);
                    return call_user_func_array(array(parent, __FUNCTION__), func_get_args());
            }
            public function offsetExists($name) { 
                    self::sdprintf("%s(%s)\n", __FUNCTION__, implode(",", func_get_args()));
                    $this->check($name);
                    return call_user_func_array(array(parent, __FUNCTION__), func_get_args());
            }
            public function offsetUnset($name) { 
                    self::sdprintf("%s(%s)\n", __FUNCTION__, implode(",", func_get_args()));
                    $this->check($name);
                    return call_user_func_array(array(parent, __FUNCTION__), func_get_args());
            }

            public function __toString() {
                    self::sdprintf("%s(%s)\n", __FUNCTION__, $name);
                    foreach ($this as $key => $value) {
                            $output .= "$key: $value\n";
                    }
                    return $output;
            }

            function __construct($array = false, $flags = 0, $iterator_class = "ArrayIterator") { 
                    self::sdprintf("%s(%s)\n", __FUNCTION__, implode(",", func_get_args()));
                    parent::setFlags(parent::ARRAY_AS_PROPS);
            }
    }
Orwellophile
  • 13,235
  • 3
  • 69
  • 45