Dave W.
Dave W.

Reputation: 1596

Is there a __get magic method equivalent for instantiating classes?

Looking to see if there is any way to implement the kind of functionality of the __get() magic method, but when trying to instantiate a new class.

My goal is to have

$foo = new Cat();

Ultimately result in the creation of a generic object, i.e.

new Mammal('Cat');

Such that $foo would be an instance of the class Mammal, with the called class name ('Cat') passed as an argument to Mammal's constructor.


Note: The end game I have in mind, for those familiar with Lithium or CakePHP, is to avoid having to define a whole bunch of classes for each different database table. If I did, most would simply be empty, the basic CRUD operations being all that is necessary. Plus, all those includes and class definitions can't be great for overhead. My idea would be to use a single "Model" class to handle most of the generic functions, and I could always create subclasses if I needed more advanced functionality for a specific database table.

Upvotes: 0

Views: 188

Answers (4)

Chris
Chris

Reputation: 455

I know this is an old question now but still relevant today for a lot of people.

My way of overcoming this exact scenario is to have a base modeler which holds all basic table interaction:

<?php
    class base {

        /**
         * @name        $table_values
         * @description This holds all data about the table including the field names and data for the record loaded.
         * @example     {
         *                  "primary_keys"  :   [],
         *                  "table_data"    :   {
         *                                          "name"  :   "value",
         *                                          "name"  :   "value"
         *                                      }
         *              }
         */
        private $table_values = array();

        /**
         * @name        __get
         * @param       $name   The name of the property to return.
         * @description The PHP magic getter.
         */
        public function __get($name) {
            $keys = array_keys($this->table_values['table_data']);
            if (in_array($name, $keys)) {
                return $this->table_values['table_data'][$name];
            }
        }

        /**
         * @name        __set
         * @param       $name   The name of the property to set.
         * @param       $value  The value to assign to the property.
         * @description The PHP magic setter.
         */
        public function __set($name, $value) {
            $this->table_values['table_data'][$name] = $value
        }

        /**
         * @name        table_name
         * @description Must override in child class. Should return the name of the table to use.
         */
        public function table_name() {}

        /**
         * @name        load
         * @param       $ids    int|array   Can be a single primary key or an assoc array of primary keys depending on the table with the keys for the array being the field names.
         * @description This method handles loading a single record from the table and storing it as a PHP object.
         */
        public function load($ids) {
            // Use the primary key(s) to find and load the record into $table_values from the database.
        }

        /**
         * @name        save
         * @description Saves the record in the database
        public function save() {
            // Use the primary key(s) to find and either insert or update the database table record.
        }
    }
?>

Use the base class as:

<?php
    class person extends base {
        public function table_name() {
            return "person";
        }
    }

    class car extends base {
        public function table_name() {
            return "car";
        }
    }
?>

etc.

Use and autoloader to automatically load classes and to handle when a class doesn't exist for a table:

<?php
    spl_autoload_register(function($class_name) {
        $filename = "/path/to/class/{$class_name}.php";
        if (class_exists($class_name)) {
            // Do nothing.
        } elesif (file_exists($filename)) {
            require_once($filename);
        } else {
            // Check if the class name exists in the database as a table.
            if ($database->table_exists($class_name)) {
                $class_def = "
class {$class_name} extends base {
    public function table_name() {
        return \"{$class_name}\";
    }
}";
                eval($class_def);
            }
        }
    });
?>

The base class can have a lot more functionality. I also have functions to override in children to do things before and after load and save, and to check for duplicate records with the same data for instance.

Upvotes: 0

Gordon
Gordon

Reputation: 316969

The way I remember it from Cake is that you define the model on top of the controller class and then simply use $this->Modelname. This should be as simple to implement as:

public function __get($prop)
{
    if(in_array($prop, $this->uses)) {
        return new $prop;
    }
}

Each time you call a non-existing property, your class would check if the property name exists in an array $uses and if so, assumes $prop is classname and instaniates it. You will want to store the instance somewhere to avoid reinstantiating each time you fetch it.

This is somewhat cleaner than writing new Klass all over the place, because that makes it hard to exchange Klass for something else. You hardwire it into the controller then. It's a dependency you want to avoid. With that said, you might want to have a look at the Symfony Dependency Injection framework.

Upvotes: 0

ircmaxell
ircmaxell

Reputation: 165201

Well, it's a bit hacky, but you could abuse the class autoloader...

So, hijack the loader to check for a class "Cat", if it doesn't exist, then eval it into existance...

if (!$foundClass) {

    $code = 'Class '.$class.' extends Mammal { 
        public function __construct() { parent::__construct("'.$class.'");}
    }';
    eval($code);
}

I said it was hacky... But you could always white-list the classes you want to do this for... Plus, it has the benefit that if you wanted to modify the class, just create a firm instance of it.

But then again, I would personally find another solution. To me, new Cat() is not readable at all. That's because of two reasons, first there's no contextual clues as to what it is and second I can't find the class Cat... What I would do is similar to what Jani suggested: $table = DatabaseTable::getInstance('cat');... I find that MUCH more readable (even though it is more characters)...

Upvotes: 0

Jani Hartikainen
Jani Hartikainen

Reputation: 43243

I don't think there is any straightforward non-hacky way to achieve what you want with the class instantiation mechanism.

I would recommend using a factory:

$tableFactory->createTable('Cat');

Which would then work out what needs to be done behind the scenes. This also has the benefit of if you ever decide Cat requires its own class, you can simply modify the factory and you may not need to modify the code that is using it.

Upvotes: 2

Related Questions