How to do member overloading with static classes

phpBB3 and MOD challenges setup by the staff to test and challenge your phpBB3 coding skills.

How to do member overloading with static classes

Postby Highway of Life » 25 May 2009, 19:17

Those in the #startrekguide Freenode IRC channel were just treated to a lesson on static class inheritance rules and how to create a static class capable of member overloading.

The first bit of code was a challenge to see if anyone could answer the question: What is the rational behind static methods being unable to use inherited members or methods.
Code: Select all
<?php

class base
{
    public $color = 'Blue';
    
    public function get_color
()
    {
        return $this->color;
    }
}

class child extends base
{
    public $color = 'Green';
}

// Static classes
class static_base
{
    public static $color = 'Red';
    
    public static function get_color
()
    {
        return self::$color;
    }
}

class static_child extends static_base
{
    public static $color = 'Purple';
}

$base = new base();
echo $base->get_color(); // Prints Blue
echo "\n<br />\n";

$child = new child();
echo $child->get_color(); // Prints Green
echo "\n<br />\n";

echo static_child::get_color(); // Prints Red -- why?
// Using the same logic as the dynamic class, shouldn't this code print Purple?
echo "\n<br />\n";
?>


Answer to the above:
Spoiler:
static method calls are resolved at compile time. When using an explicit class name the method is already identified completely and no inheritance rules apply. If the call is done by self then self is translated to the current class, that is the class the code belongs to. Here also no inheritance rules apply.


Is there a workaround? There really are no 'Good' workarounds without duplicating code.
There are three workarounds that I'm aware of, but not great solutions except #1.
  1. Use a dynamic class instead of a static class.

  2. Set a member variable in the constructor. This makes the value available within the object as well as from the outside, though it is not static. Obviously not a great solution.

  3. The third way is to pass an extra class name parameter into the static method calls telling the method which class it should 'act as.' Again, not a great solution.
In PHP 5.3, it is now possible to use Late-Static Bindings, but you have to be deliberate about it when creating your code.

The second part of the lesson was how to do overloading in PHP. Most programmers are aware of the typical method of doing member overloading using the magic methods: __get, __set and __isset. But this is not possible with a static method. That is not to worry though, because there is still a way to do this with static classes, here is an example where we create a simple API to get a list of posts based on post time.

Enjoy!

Code: Select all
<?php

class base
{
    public static $object_data = array();
    
    public static $classname 
= __CLASS__;

    public static function get($arg)
    {
        if (!isset(self::$object_data[$arg]))
        {
            $args = func_get_args();
            call_user_func_array(__CLASS__ . '::' . 'set', $args);
        }
        
        return self
::$object_data[$arg];
    }
    
    public static function set
($arg)
    {
        $args = func_get_args();
        
        if 
(!method_exists(self::$classname, 'get_' . $arg))
        {
            self::$object_data[$arg] = $args[1];
        }
        else 
        
{
            $args = array_shift($args);
            self::$object_data[$arg] = call_user_func_array(self::$classname . '::' . 'get_' . $arg, $args);
        }
    }
}

// Lets give an example based on something you might do with a phpBB type project

class posts extends base
{
    public static function set_class()
    {
        parent::$classname = __CLASS__;
    }

    public static function get_posts()
    {
        global $db;

        $limit = self::get('limit', 100);
        $start = self::get('start', 0);

        $sql = $db->sql_build_query('SELECT', self::base_sql());
        $result = $db->sql_query_limit($sql, $limit, $start);

        while ($row = $db->sql_fetchrow($result))
        {
            $post_data[$row['post_id']] = $row;
        }
        
        return $post_data
;
    }

    // Build sql query...
    public static function base_sql()
    {
        $start_time = self::get('start_time', time());
        $end_time    = self::get('end_time', time());
        $order_by    = self::get('order_by', 'p.post_time');
        $sort_order = self::get('sort_order', 'DESC');

        // typically, we would not use sql_ary for a single-table query
        // but when building a query, this allows us to modify the parameters 'later' making it extensible
        $sql_ary = array(
            'SELECT'    => 'p.*',
            'FROM'        => array(POSTS_TABLE => 'p'),
            'WHERE'        => 'p.post_time BETWEEN ' . (int) $start_time . ' AND ' . (int) $end_time,
            'ORDER_BY'    => $order_by . ' ' . $sort_order, // if this is user input, we would need to sanitize it
        );

        return $sql_ary;
    }
}

$date = getdate();

posts::set_class();

// Get posts within the last 7 days
posts::set('start_time', mktime(0, 0, 0, $date['mon'], $date['mday'] - 7, $date['year']));
posts::set('end_time', $date[0]);

// Show only 10 posts
posts::set('limit', 10);

$posts_ary = posts::get('posts');

foreach ($posts_ary as $post_id => $row)
{
    $template->assign_block_vars('posts', array(
        // Post data...
    ));
}
  
Watch out! I might do a code wheelie!

User avatar
Highway of Life    
STG Jedi Master
STG Jedi Master
 
Posts: 10458
Joined: 08 May 2006, 05:23
Location: Beware of Programmers carrying screwdrivers
Gender: Male
phpBB Knowledge: 10




phpBB Academy at StarTrekGuide
Support STG
Using PayPal Donate

Re: How to do member overloading with static classes

Postby topdown » 25 May 2009, 20:47

Why not go straight for the $apple and skip the fruit(); search?
Using the Resolution Operator :: and calling the $var through the class
echo static_child::$color; //Prints purple

Spoiler:
Code: Select all
<?php

class base
{
    public $color = 'Blue';

    public function get_color()
    {
        return $this->color;
    }
}

class child extends base
{
    public $color = 'Green';
}

// Static classes
class static_base
{
    public static $color = 'Red';

    public static function get_color()
    {
        return self::$color;
    }
}

class static_child extends static_base
{
    public static $color = 'Purple';
}

$base = new base();
echo $base->get_color(); // Prints Blue
echo "\n<br />\n";

$child = new child();
echo $child->get_color(); // Prints Green
echo "\n<br />\n";
    

echo static_child
::get_color(); // Prints Red -- why?
// Using the same logic as the dynamic class, shouldn't this code print Purple?
echo "\n<br />\n";
        
echo static_child
::$color; // Prints Purple
 


The second code I can't seem to use
Errors that I can't get rid of, assuming it's from PHP 5.2.6 :glare:
Spoiler:
Code: Select all
Fatal error: Cannot access self:: when no class scope is active in G:\xampp\htdocs\webmasters\includes\classes\posts.php on line 78

I get an undefined offset 1 also

Should this
Code: Select all
// Get posts within the last 7 days
self::set('start_time', mktime(0, 0, 0, $date['mon'], $date['mday'] - 7, $date['year']));
self::set('end_time', $date[0]); 

be
Code: Select all
// Get posts within the last 7 days
posts::set('start_time', mktime(0, 0, 0, $date['mon'], $date['mday'] - 7, $date['year']));
posts::set('end_time', $date[0]); 
Do not PM me for Support unless I give permission in a post......PM's only help one, posts help everyone !
User avatar
topdown    
STG Styles Leader
STG Styles Leader
 
Posts: 3030
Joined: 01 Oct 2007, 22:56
Location: Handyman's harddrive
Favorite Team: STG Teams
Gender: Male
phpBB Knowledge: 9

Re: How to do member overloading with static classes

Postby Highway of Life » 25 May 2009, 23:59

Kudos to actually testing the code, most don't. And although I pasted this code without testing it because it was merely an example/demonstration, it should be correct regardless. :)

About 5 minutes of looking at the code told me four problems occurring there.

  1. When outside the object context, use the class name, you can never use 'self', which was the case here... curse of copying code from my class and pasting outside the object but not thinking to change the self to the class name, and secondly... not testing this code.

  2. The get_posts method needed to return the array, not set it, this would have resulted in a segfault.

  3. The undefined index was as a result of using function_exists incorrectly. function_exists does not work on object methods. A "function" within an object is a method, not a function. The correct function for me to use there is method_exists.

  4. Additionally, because I've separated the posts object from the base object, it's important for the get and set methods to be extensible, because of this, the class name has to be able to be set by a child object, to do this effectively, I created a $classname member of the base class, and a method in the child called set_class that sets the parent::$classname member with the current child class name, this effectively allow me to use the get and set methods from within any child method. This was one of the workarounds described previously.


----oOo----


Regarding the first bit of code, you have to remember that it's a simple example to demonstrate a sophisticated problem.
Obviously you can just grab the member directly, but let me give an example in phpBB 3.2 where this could be a problem.
Lets say you want to modify the DBAL for your website, but instead of modifying the DBAL directly, you create a child object that extends the parent object (dbal_mysqli) and make your modifications to that method.
Now your common points to your class instead of the dbal_mysqli to use your code, but the problem comes from within dbal_mysqli when it calls that function using self:: (because it's a static class), it's not going to use your method because it's in a child object, it's going to use the method that exists there. So to solve this you have two options:
  1. Duplicate ALL of the methods within that class that call your modified function.

  2. Make the changes directly to the core, which was precisely what you were attempting to avoid in the first place, and is important when building websites around phpBB3.
Additionally, it's a simple example of what would otherwise by a much more complex object created, if you don't know what the $fruit is (an $apple), you need fruit() to find it...

BTW, the code in the first post has been fixed.
Watch out! I might do a code wheelie!

User avatar
Highway of Life    
STG Jedi Master
STG Jedi Master
 
Posts: 10458
Joined: 08 May 2006, 05:23
Location: Beware of Programmers carrying screwdrivers
Gender: Male
phpBB Knowledge: 10

Re: How to do member overloading with static classes

Postby Erik Frèrejean » 26 May 2009, 05:41

Sad I've missed that discussion :bye:.
Your first example isn't much news under the sun (at least for me) the second one looks interesting though. If I have some more time I'll need to look closer at that snippet :).
Image Proud member of the phpBB support team
Image STG Support team member | Image STG Moderator team member
Image
User avatar
Erik Frèrejean    
phpBB Team Member
phpBB Team Member
 
Posts: 1114
Joined: 03 Dec 2007, 00:49
Location: USERS_TABLE
Favorite Team: New Orleans Saints
Gender: Male
phpBB Knowledge: 10

Re: How to do member overloading with static classes

Postby Erik Frèrejean » 31 May 2009, 06:52

I just had a closer look at David his static overloading example and it allows really more flexibility, and its an implementation I never though about so thanks for sharing :good:.

I however found it a bit to limiting because you can only extend the base class by one child at the time (or continually switch the "class name"), which is kinda limiting cause why have it extend it than in the first place ;). So I've rewritten the base class in a way that you can have more classes extend it.
Spoiler:
I'm not sure about the performance of this :this:

Anyhow:
Code: Select all
class base
{
    /**
     * All child classes
     * @var String
     */
    protected static $classnames = array(
        __CLASS__,
    );
    
    
/**
     * All the data
     * @var mixed
     */
    protected static $object_data = array();

    public static function get($arg)
    {
        if (!isset(self::$object_data[$arg]))
        {
            $args = func_get_args();
            call_user_func_array(__CLASS__ . '::' . 'set', $args);
        }
        
        return self
::$object_data[$arg];
    }
    
    public static function set
($arg)
    {
        $args = func_get_args();

        // First we'll try to determine whether there is a child with a method we "can" use
        foreach (self::$classnames as $classname)
        {
            if (method_exists($classname, 'get_' . $arg))
            {
                $args = array_shift($args);
                self::$object_data[$arg] = call_user_func_array($classname. '::' . 'get_' . $arg, $args);
                return;
            }
        }
        
        if 
(isset($args[1]))
        {
            self::$object_data[$arg] = $args[1];
        }
    }
}
 

The child classes also need a small change. Instead of:
Code: Select all
      public static function set_class()
    {
          parent::$classname = __CLASS__;
    } 

this function has to look like:
Code: Select all
    public static function set_classname()
    {
        parent::$classnames[] = __CLASS__;
    } 
Last edited by Erik Frèrejean on 31 May 2009, 07:14, edited 1 time in total.
Reason: Typo
Image Proud member of the phpBB support team
Image STG Support team member | Image STG Moderator team member
Image
User avatar
Erik Frèrejean    
phpBB Team Member
phpBB Team Member
 
Posts: 1114
Joined: 03 Dec 2007, 00:49
Location: USERS_TABLE
Favorite Team: New Orleans Saints
Gender: Male
phpBB Knowledge: 10

Re: How to do member overloading with static classes

Postby Highway of Life » 14 Jun 2009, 13:30

The way that I solve that issue is that when I am calling my set method, right before I use a get or set, I set the classname. I don't think it's a good idea if the class searches through an array of classes, especially since the set method has two different functionalities, one is dependent on the method not existing.
Code: Select all
class posts extends base
{
    public static function get_posts()
    {
        global $db;
        
        self
::set_classname(__CLASS__);

        $limit = self::get('limit', 100);
        $start = self::get('start', 0);

        $sql = $db->sql_build_query('SELECT', self::base_sql());
        $result = $db->sql_query_limit($sql, $limit, $start);

        while ($row = $db->sql_fetchrow($result))
        {
            $post_data[$row['post_id']] = $row;
        }
        
        return $post_data
;
    } 
Watch out! I might do a code wheelie!

User avatar
Highway of Life    
STG Jedi Master
STG Jedi Master
 
Posts: 10458
Joined: 08 May 2006, 05:23
Location: Beware of Programmers carrying screwdrivers
Gender: Male
phpBB Knowledge: 10


Return to phpBB3 Challenges at phpBB Academy

Who is online

Users browsing this forum: No registered users and 5 guests