2
\$\begingroup\$

I have a project in my mind for quite some time, and it requires me to create dynamic objects.

Usually, casting an array to object does the trick (e.g.: (object)array('test'=>5)). But sometimes, we still want the magic methods to fire up. For that, I have developed a class.

It supports the magic methods __construct, __get, __set, __isset and __unset. This also has a method to check the keys in your object.

class Object {
    private $props = array();
    private $methods = array(
        '__construct'=>null,
        '__get'=>null,
        '__set'=>null,
        '__isset'=>null,
        '__unset'=>null
    );

    private function __run_magic_method( $method, $args=null )
    {
        $obj = (object)$this->props;
        $return = call_user_func_array( $this->methods[$method], array_merge( array(&$obj), (array)$args ) );
        $this->props = (array)$obj;
        return $return;
    }

    private function __has_magic_method( $method )
    {
        return isset( $this->methods[$method] ) && is_callable( $this->methods[$method] );
    }

    function __construct( array $props )
    {
        foreach( array_keys($this->methods) as $method )
        {
            if( isset( $props[$method] ) )
            {
                $this->methods[$method] = $props[$method];
                unset( $props[$method] );
            }
        }

        $this->props = $props;

        if( $this->__has_magic_method( '__construct' ) )
        {
            $this->__run_magic_method( '__construct' );
        }
    }

    function __get( $key )
    {
        if($this->__has_magic_method('__get' ) )
        {
            return $this->__run_magic_method( '__get', array($key) );
        }
        else
        {
            return $this->props[$key];
        }
    }

    function __set( $key, $value )
    {
        if( $this->__has_magic_method( '__set' ) )
        {
            $this->__run_magic_method( '__set', array( $key, $value ) );
        }
        else
        {
            $this->props[$key] = $value;
        }
    }

    function __isset( $key )
    {
        if( $this->__has_magic_method( '__isset' ) )
        {
            return $this->__run_magic_method( '__isset', array($key) );
        }
        else
        {
            return isset( $this->props[$key] );
        }
    }

    function __unset( $key )
    {
        if( $this->__has_magic_method( '__unset' ) )
        {
            return $this->__run_magic_method( '__unset', array($key) );
        }
        else
        {
            unset( $this->props[$key] );
        }
    }

    function __call( $key, $args )
    {
        if( isset( $this->props[$key] ) && is_callable( $this->props[$key] ) )
        {
            return call_user_func_array( $this->props[$key], $args );
        }
        else
        {
            throw new Exception( 'The key "'.addslashes($key).'" is not a callable' );
        }
    }

    private function __keys()
    {
        return array_keys( $this->props );
    }

    static function keys( Object $obj=null )
    {
        if( $obj === null )
        {
            return array();
        }
        else
        {
            return $obj->__keys();
        }
    }
}

Example of usage:

$obj = new Object(array(
    '__construct'=>function(){echo 'constructor executed', PHP_EOL;},
    '__isset'=>function($t,$k){
        echo 'checking key: ', $k, PHP_EOL;
        return isset($t->{$k});
    },
    'test'=>5
));

if( isset($obj->test) )
{
    echo $obj->test;
}

/*
Should output:

constructor executed
checking key: test
5
*/

What can I improve in this code, in terms of readability? What crimes am I committing with this code?

\$\endgroup\$
3
  • \$\begingroup\$ "PHP reserves all function names starting with __ as magical. It is recommended that you do not use function names with __ in PHP unless you want some documented magic functionality." (php.net/manual/en/language.oop5.magic.php) \$\endgroup\$
    – Agares
    Apr 6, 2015 at 13:04
  • \$\begingroup\$ @Agares I am aware of that. I should have used _ as a prefix instead. \$\endgroup\$ Apr 6, 2015 at 13:06
  • \$\begingroup\$ @Agares I've added it on github (github.com/ismael-miguel/object-class-php) with a few changes. The only changes were the one in your comment, removed the exception and now I use a trigger_error instead and added support for the magic method __toString. \$\endgroup\$ Apr 30, 2015 at 11:45

1 Answer 1

1
\$\begingroup\$

As pointed out before, in a comment, non-magic methods in a class shouldn't be prefixed with __.

Since they are private, they are prefixed with _.


Closures are a really nice feature that came with PHP5.3, and they weren't being used to their full potential.

Closures have the method bindTo, which generates a new Closure binded to that object.
This allows to use the variable $this inside the Closure.
It helps the fellow programmers that decide to use this to write in a more natural way.
The best part is that there are no kludges needed!

The method _run_magic_method() will look like this:

    private function _run_magic_method( $method, $args=null )
    {
        $obj = (object)$this->props;

        if( is_a( $this->methods[$method], 'Closure' ) )
        {
            $return = call_user_func_array( $this->methods[$method]->bindTo( $obj ), (array)$args );
        }
        else
        {
            $return = call_user_func_array( $this->methods[$method], array_merge( array(&$obj), (array)$args ) );
        }
        $this->props = (array)$obj;
        return $return;
    }

Since objects are passed as references in PHP5, all changes are reflected on the properties.


The final code:

<?php

    class Object {
        private $props = array();
        private $methods = array(
            '__construct'=>null,
            '__get'=>null,
            '__set'=>null,
            '__isset'=>null,
            '__unset'=>null,
            '__toString'=>null
        );
        private function _run_magic_method( $method, $args=null )
        {
            $obj = (object)$this->props;

            if( is_a( $this->methods[$method], 'Closure' ) )
            {
                //Inside the closure, the $this will be changing the parent $this->props
                $return = call_user_func_array( $this->methods[$method]->bindTo( $obj ), (array)$args );
            }
            else
            {
                $return = call_user_func_array( $this->methods[$method], array_merge( array( &$obj ), (array)$args ) );
            }

            $this->props = (array)$obj;
            return $return;
        }
        private function _has_magic_method( $method )
        {
            return isset( $this->methods[$method] ) && is_callable( $this->methods[$method] );
        }
        function __construct( array $props )
        {
            foreach( array_keys( $this->methods ) as $method )
            {
                if( isset( $props[$method] ) )
                {
                    $this->methods[$method] = $props[$method];
                    unset( $props[$method] );
                }
            }
            $this->props = $props;
            if( $this->_has_magic_method( '__construct' ) )
            {
                $this->_run_magic_method( '__construct' );
            }
        }
        function __get( $key )
        {
            if($this->_has_magic_method('__get' ) )
            {
                return $this->_run_magic_method( '__get', array($key) );
            }
            else
            {
                return $this->props[$key];
            }
        }
        function __set( $key, $value )
        {
            if( $this->_has_magic_method( '__set' ) )
            {
                $this->_run_magic_method( '__set', array( $key, $value ) );
            }
            else
            {
                $this->props[$key] = $value;
            }
        }
        function __isset( $key )
        {
            if( $this->_has_magic_method( '__isset' ) )
            {
                return $this->_run_magic_method( '__isset', array($key) );
            }
            else
            {
                return isset( $this->props[$key] );
            }
        }
        function __unset( $key )
        {
            if( $this->_has_magic_method( '__unset' ) )
            {
                return $this->_run_magic_method( '__unset', array($key) );
            }
            else
            {
                unset( $this->props[$key] );
            }
        }
        function __call( $key, $args )
        {
            if( isset( $this->props[$key] ) && is_callable( $this->props[$key] ) )
            {
                return call_user_func_array( $this->props[$key], $args );
            }
            else
            {
                trigger_error( 'The key "'.addslashes($key).'" is not a callable', E_USER_ERROR );
            }
        }
        function __toString( )
        {
            if( $this->_has_magic_method( '__toString' ) )
            {
                return $this->_run_magic_method( '__toString' ) . '';
            }
            else
            {
                trigger_error( 'There is no defined __toString method', E_USER_ERROR );
            }
        }
        private function _keys()
        {
            return array_keys( $this->props );
        }
        static function keys( Object $obj=null )
        {
            if( $obj === null )
            {
                return array();
            }
            else
            {
                return $obj->_keys();
            }
        }
    }

Example of execution:

$obj = new Object(array(
    'a'=>5,
    '__get'=>function($k){
        echo 'Getting the value from the key \'',$k,'\'', PHP_EOL;
        return $this->{$k};
    },
    '__set'=>function($k,$v){
        echo 'Setting a value on the key \'',$k,'\'', PHP_EOL;
        $this->{$k}=$v;
    }
));

/*
 *Should output:
 *
 * Getting the value from the key 'a'
 * 5
 * Setting a value on the key 'b'
 * Getting the value from the key 'b'
 * 6
 *
 */
\$\endgroup\$
1
  • \$\begingroup\$ Since the number of other answers was null, I'm (sadly) marking this as the accepted answer. \$\endgroup\$ Jun 17, 2015 at 10:40

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.