Page 1 of 3

Extending PHP method/property modifiers

Posted: Thu Oct 16, 2008 2:57 am
by VladSun
The idea comes from viewtopic.php?f=19&t=89300 discussion.
It's an object "wrapper" class ("decorator"?).

Code: Select all

define('PROPERTY_WRITABLE', true);
define('PROPERTY_NOT_WRITABLE', false);
define('PROPERTY_READABLE', true);
define('PROPERTY_NOT_READABLE', false);
define('PROPERTY_NULLABLE', true);
define('PROPERTY_NOT_NULLABLE', false);
 
define('METHOD_EXECUTABLE', true);
define('METHOD_NOT_EXECUTABLE', false);
 
define('MODE_STRICT', true);
define('MODE_NOT_STRICT', false);
 
interface IACL
{
    function isPropertyWritable($class, $property);
    function isPropertyReadable($class, $property);
    function isPropertyNullable($class, $property);
    
    function isMethodExecutable($class, $method);
}
 
class NullObject
{
    public function __call($method, $args) 
    {
        return new NullObject();
    }
    
    public function __get($var)
    {
        return new NullObject();
    }
     
    public function __set($var, $value)
    {
    } 
     
    public function __toString()
    {
        return '';
    }
}
 
class ProtectedPropertyException extends Exception
{
    private $_objectName = null;
    private $_propertyName = null;
    
    public function __construct($message, $objectName, $propertyName) 
    {
        parent::__construct($message, 0);
        $this->_objectName = $objectName;
        $this->_propertyName = $propertyName;
    }   
 
    public function __toString() 
    {
        return ": [{$this->_objectName}::{$this->_propertyName}]: {$this->message}\n";
    }
}
 
class ProtectedMethodException extends Exception
{
    private $_objectName = null;
    private $_methodName = null;
    
    public function __construct($message, $objectName, $methodName) 
    {
        parent::__construct($message, 0);
        $this->_objectName = $objectName;
        $this->_methodName = $methodName;
    }   
 
    public function __toString() 
    {
        return ": [{$this->_objectName}::{$this->_methodName}]: {$this->message}\n";
    }
}
 
class ProtectedProperty
{
    private $_isReadable = false;
    private $_isWritable = false;
    private $_isNullable = false;
    private $_strictMode = false;
    
    private $_object = null;    
    private $_propertyName = null;
 
    public function __toString()
    {
        return $this->get()->__toString();
    }
 
    public function __construct(&$object, 
                                $propertyName, 
                                $value = null, 
                                $isReadable = PROPERTY_READABLE, 
                                $isWritable = PROPERTY_WRITABLE, 
                                $isNullable = PROPERTY_NULLABLE,
                                $strictMode = MODE_NOT_STRICT)
    {
        $this->_object =& $object;
        $this->_propertyName = $propertyName;
        
        $this->_isReadable = $isReadable;
        $this->_isWritable = $isWritable;
        $this->_isNullable = $isNullable;
        
        $this->_strictMode = $strictMode;
        
        if ($this->_isNullable !== PROPERTY_NULLABLE && $value === null)
        {
            if ($this->_strictMode == MODE_STRICT)
                throw new ProtectedPropertyException('Property is not nullable.', get_class($this->_object), $this->_propertyName);
            else
                $this->_object->{$this->_propertyName} = $value;
        }
        else
            $this->_object->{$this->_propertyName} = $value;
 
    }
 
    public function get()
    {
        if ($this->_isReadable !== PROPERTY_READABLE)
        {
            if ($this->_strictMode === MODE_STRICT)
                throw new ProtectedPropertyException('Property is not readable.', get_class($this->_object), $this->_propertyName);
            else
                return new NullObject();
        }       
    
        return $this->_object->{$this->_propertyName};      
    }
     
    public function set($object)
    {
        if ($this->_isWritable !== PROPERTY_WRITABLE)
        {
            if ($this->_strictMode === MODE_STRICT)
                throw new ProtectedPropertyException('Property is not writable.', get_class($this->_object), $this->_propertyName);
            else
                return false;
        }
 
        if ($this->_isNullable !== PROPERTY_NULLABLE && $object == null)
        {
            if ($this->_strictMode === MODE_STRICT)
                throw new ProtectedPropertyException('Property is not nullable.', get_class($this->_object), $this->_propertyName);
            else
                return false;
        }
 
        $this->_object->{$this->_propertyName} = $object;
        return true;        
    } 
    
    public function isReadable()
    {
        return $this->_isReadable === PROPERTY_READABLE;
    }
    
    public function isWritable()
    {
        return $this->_isWritable === PROPERTY_WRITABLE;
    }
 
    public function isNullable()
    {
        return $this->_isWritable === PROPERTY_NULLABLE;
    }
}
 
class ProtectedMethod
{
    private $_isExecutable = false;
    private $_strictMode = false;
    
    private $_object = null;    
    private $_methodName = null;
 
    public function __construct(&$object, 
                                $methodName, 
                                $isExecutable = METHOD_EXECUTABLE, 
                                $strictMode = MODE_NOT_STRICT)
    {
        $this->_object =& $object;
        $this->_methodName = $methodName;
        $this->_isExecutable = $isExecutable;
        $this->_strictMode = $strictMode;
    }
    
    public function execute($args)
    {
        if ($this->_isExecutable !== METHOD_EXECUTABLE)
            if ($this->_strictMode === MODE_STRICT)
                throw new ProtectedMethodException(' Method is not executable.', get_class($this->_object), $this->_methodName);
            else
                return new NullObject();
                
        $method = new ReflectionMethod(get_class($this->_object), $this->_methodName);
        return $method->invoke($this->_object, $args);
    }
    
    public function isExecutable()
    {
        return $this->_isExecutable;
    }
}
 
class ProtectedObject
{
    public $_object = null;
    private $_class = null;
    private $_strictMode = MODE_NOT_STRICT;
    
    private $_properties = Array();
    private $_methods = Array();
    
    public function __call($method, $args) 
    {
        return $this->_methods[$method]->execute($args);
    }
    
    public function __get($var)
    {
        return $this->_properties[$var]->get();
    }
     
    public function __set($var, $value)
    {
        $this->_properties[$var]->set($value);
    } 
     
    public function __construct($class_name, $ACL, $strictMode = MODE_NOT_STRICT)
    {
        $this->_class = new ReflectionClass($class_name);
        $this->_object = $this->_class->newInstance();
        $this->_strictMode = $strictMode;
 
        foreach ($this->_class->getProperties() as $property)
        {
            if ($property->isPublic())
            {
                $this->_properties[$property->getName()] = new ProtectedProperty($this->_object, $property->getName(), $property->getValue($this->_object),
                $ACL->isPropertyReadable($this->_class->getName(), $property->getName()), 
                $ACL->isPropertyWritable($this->_class->getName(), $property->getName()),
                $ACL->isPropertyNullable($this->_class->getName(), $property->getName()),
                $this->_strictMode);  
            }
        }
        
        foreach ($this->_class->getMethods() as $method)
        {
            if ($method->isPublic())
            {
                $this->_methods[$method->getName()] = new ProtectedMethod($this->_object, $method->getName(), 
                $ACL->isMethodExecutable($this->_class->getName(), $method->getName()),
                $this->_strictMode
                );
            }
        }
    }
}
 
class ProtectedObjectProvider
{
    private static $_instance = null;
    private $_strictMode = MODE_NOT_STRICT;
    private $_ACL = null;
    
    public function setACL($ACL)
    {
        if ($ACL == null)
            throw new Exception('ACL object can not be NULL.');         
 
        $this->_ACL = $ACL;
            
        $class = new ReflectionClass(get_class($this->_ACL));
        if (!$class->implementsInterface('IACL'))
        {
            $this->_ACL = null;
            throw new Exception('ACL object must implement IACL interface.');           
        }
    }
    
    public function setMode($strictMode)
    {
        $this->_strictMode = $strictMode;
    }
    
    public static function getInstance()
    {
        if(!isset(self::$instance))
        {
            $object = __CLASS__;
            self::$_instance = new $object;
        }
        return self::$_instance;
    }
    
    public function construct($class, $strictMode = null)
    {
        if (is_string($class))
            return new ProtectedObject($class, $this->_ACL, $strictMode == null ? $this->_strictMode : $strictMode);
        if (is_object($class))
            return new ProtectedObject(get_class($class), $this->_ACL, $strictMode == null ? $this->_strictMode : $strictMode);
            
        throw new Exception('Could not construct protected object from data <'.var_export($class, true).'>');
    }
}
Any comments on its design, implementation and usability are welcome :)

EDIT: Some bugs fixed

Re: Extending PHP method/property modifiers

Posted: Thu Oct 16, 2008 5:24 pm
by VladSun
Well, the wrapper does not implement object constants and other things. It's a "known bug" which is should be considered not in the scope of current discussion :)

Re: Extending PHP method/property modifiers

Posted: Fri Oct 17, 2008 1:01 am
by alex.barylski
From what I understood of this disscussion last time...you were trying to implement an ACL system of sorts, using reflection for some reason or another?

I think I see now wht you wanted to use reflection. From just a quick browse of the code it seems you are using reflection and PHP's default access control modifiers to determine whether the function executes. Dude, your insane. :P

I can tell you spend a lot of time at the Linux system level tinkering. Not just becuase you always help me with my Linux woes but because you are implementing system level security at the application level. :P

Do I understand correctly, are you implementing an ACL system so fine grained, that you control whihc *functions* get executed or not?

Do you actually have a requirement for something so precise? I'm sure there are some situations, but personally I'd be worried about affecting the over all user experience with that tight of control on everything. It's hard/confusing enough managing roles and permission tables at a higher level events/controllers.

Some observations:

This method on line 170:

Code: Select all

 
public function isNullable()
{
  return $this->_isWritable === PROPERTY_NULLABLE;
}
 
Shouldn't that test isNullable?

I noticed you have a few 'references' in your code. PHP5 that isn't required all objects are passed as/by reference. If you need to copy a object by value you actually have to explicitly __clone() it.


p.s-I like your coding style. Very similar to my own. I almost sense a C/C++ background? :)

p.p.s-Some usage examples would be nice so I better understand how this code would be used rather than going over the implementation (ie: unit tests) :P

Interesting idea for sure...

Cheers,
Alex

Re: Extending PHP method/property modifiers

Posted: Fri Oct 17, 2008 4:30 am
by VladSun
PCSpectra wrote:From what I understood of this disscussion last time...you were trying to implement an ACL system of sorts, using reflection for some reason or another?

I think I see now wht you wanted to use reflection. From just a quick browse of the code it seems you are using reflection and PHP's default access control modifiers to determine whether the function executes. Dude, your insane. :P
I use default access control modifiers to determine if the property/method belongs to the object "API" - that is, is it public or not.
PCSpectra wrote:I can tell you spend a lot of time at the Linux system level tinkering. Not just becuase you always help me with my Linux woes but because you are implementing system level security at the application level. :P

Do I understand correctly, are you implementing an ACL system so fine grained, that you control whihc *functions* get executed or not?

Do you actually have a requirement for something so precise? I'm sure there are some situations, but personally I'd be worried about affecting the over all user experience with that tight of control on everything. It's hard/confusing enough managing roles and permission tables at a higher level events/controllers.
The solution is to use default policy for an object :)
I.e., in my ACL ony the permitions which differ from the default policy permissions are defined.
PCSpectra wrote:Some observations:

This method on line 170:

Code: Select all

 
public function isNullable()
{
  return $this->_isWritable === PROPERTY_NULLABLE;
}
 
Shouldn't that test isNullable?
Obviously :) A copy-paste issue ;)
PCSpectra wrote:I noticed you have a few 'references' in your code. PHP5 that isn't required all objects are passed as/by reference. If you need to copy a object by value you actually have to explicitly __clone() it.
PHP4 bad habbits :)
PCSpectra wrote:p.s-I like your coding style. Very similar to my own. I almost sense a C/C++ background? :)
A lot of C/C++ ;)
PCSpectra wrote:p.p.s-Some usage examples would be nice so I better understand how this code would be used rather than going over the implementation (ie: unit tests) :P
I'll do it in my next post :)

Re: Extending PHP method/property modifiers

Posted: Fri Oct 17, 2008 9:04 pm
by Christopher
Hey Vlad. Can you post some example code or tests that show the code above working?

Re: Extending PHP method/property modifiers

Posted: Sun Oct 19, 2008 5:46 am
by koen.h
I'm interested in examples too.

Am I right that with this system, the only way to get x number of objects that the user is allowed to read out of the db, is to get them one by one, then check permission and get the next one until at x?

Re: Extending PHP method/property modifiers

Posted: Sun Oct 19, 2008 6:16 am
by Weirdan
This approach should be quite expensive resource-wise... I'm interested to see benchmarks =)

The reason to ask is that I implemented a quite similar solution once. We were passing model objects to templates, but wanted to limit the list of methods template authors were able to use. It turned out to be very expensive to use such wrapper - rendering times increased significantly (I don't remember exact numbers, but it was like 20%-30% slowdown).

Re: Extending PHP method/property modifiers

Posted: Sun Oct 19, 2008 6:18 pm
by VladSun
OK ... some examples:

Code: Select all

 
define('BM_PROPERTY_WRITABLE', 1);
define('BM_PROPERTY_READABLE', 2);
define('BM_PROPERTY_NULLABLE', 4);
define('BM_PROPERTY_NOT_WRITABLE', 0);
define('BM_PROPERTY_NOT_READABLE', 0);
define('BM_PROPERTY_NOT_NULLABLE', 0);
 
define('BM_METHOD_EXECUTABLE', 1);
define('BM_METHOD_NOT_EXECUTABLE', 0);
 
 
$roles = Array();
 
$roles['user'] = Array();
$roles['user']['object'] = Array();
 
$roles['user']['object']['UserProfile'] = Array();
$roles['user']['object']['UserProfile']['property'] = Array();
$roles['user']['object']['UserProfile']['property']['id'] = BM_PROPERTY_READABLE;
$roles['user']['object']['UserProfile']['property']['password'] = BM_PROPERTY_NOT_READABLE + BM_PROPERTY_NOT_WRITABLE + BM_PROPERTY_NOT_NULLABLE;
$roles['user']['object']['UserProfile']['property']['name'] = BM_PROPERTY_READABLE + BM_PROPERTY_NOT_WRITABLE + PROPERTY_NOT_NULLABLE;
$roles['user']['object']['UserProfile']['property']['address'] = BM_PROPERTY_READABLE + BM_PROPERTY_WRITABLE + BM_PROPERTY_NULLABLE;
 
$roles['user']['object']['UserProfile']['method'] = Array(); 
$roles['user']['object']['UserProfile']['method']['load'] = METHOD_EXECUTABLE; 
$roles['user']['object']['UserProfile']['method']['save'] = METHOD_NOT_EXECUTABLE; 
 
 
$roles['admin'] = Array();
$roles['admin']['object'] = Array();
 
$roles['admin']['object']['UserProfile'] = Array();
$roles['admin']['object']['UserProfile']['property'] = Array();
$roles['admin']['object']['UserProfile']['property']['id'] = BM_PROPERTY_READABLE + BM_PROPERTY_NOT_WRITABLE + BM_PROPERTY_NOT_NULLABLE;
$roles['admin']['object']['UserProfile']['property']['password'] = BM_PROPERTY_NOT_READABLE + BM_PROPERTY_WRITABLE + PROPERTY_NOT_NULLABLE;
$roles['admin']['object']['UserProfile']['property']['name'] = BM_PROPERTY_READABLE + BM_PROPERTY_WRITABLE + PROPERTY_NOT_NULLABLE;
$roles['admin']['object']['UserProfile']['property']['address'] = BM_PROPERTY_READABLE + BM_PROPERTY_WRITABLE + BM_PROPERTY_NULLABLE;
 
$roles['admin']['object']['UserProfile']['method'] = Array(); 
$roles['admin']['object']['UserProfile']['method']['load'] = METHOD_EXECUTABLE; 
$roles['admin']['object']['UserProfile']['method']['save'] = METHOD_EXECUTABLE; 
 
class ACL implements IACL
{
    private static $_instance = null;
    private $_role = '';
    
    public function setRole($roleName)
    {
        $this->_role = $roleName; 
    }
    
    public function getRole()
    {
        return $this->_role; 
    }
    
    public static function getInstance()
    {
        if(!isset(self::$_instance))
        {
            $object= __CLASS__;
            self::$_instance=new $object;
        }
        return self::$_instance;
    }
    
    public function isPropertyWritable($class, $property)
    {
        global $roles;
        return ($roles[$this->_role]['object'][$class]['property'][$property] & BM_PROPERTY_WRITABLE) > 0;
    }
 
    public function isPropertyReadable($class, $property)
    {
        global $roles;
        return ($roles[$this->_role]['object'][$class]['property'][$property] & BM_PROPERTY_READABLE) > 0;
    }
 
    public function isPropertyNullable($class, $property)
    {
        global $roles;
        return ($roles[$this->_role]['object'][$class]['property'][$property] & BM_PROPERTY_NULLABLE) > 0;
    }
 
    public function isMethodExecutable($class, $method)
    {
        if ($class === $method)
            return true;
            
        global $roles;
        return ($roles[$this->_role]['object'][$class]['method'][$method] & BM_METHOD_EXECUTABLE) > 0;
    }
}
 
class UserProfile
{
    public $id = 0;
    public $name = '';
    public $password = '';
    public $address = '';
    
    public function load($id)
    {
        $row = DB::query('
            select 
                * 
            from 
                user 
            where id='.$id
        );
        
        $this->id = $id;
        $this->name = $row['name'];
        $this->password = $row['password'];
        $this->address = $row['address'];
    }
    
    public function save()
    {
        DB::query("
            update 
                user 
            set 
                name='".$this->name."',
                password='".$this->password."',
                address='".$this->address."'
            where
                id = ".$this->id
        );
    }
}
 
$objectProvider = ProtectedObjectProvider::getInstance();
$ACL = ACL::getInstance();
 
$objectProvider->setACL($ACL);
$ACL->setRole('user');
 
$userProfile = $objectProvider->construct('UserProfile', MODE_NOT_STRICT);
//$userProfile->load(1);
 
// View?
echo "id:       ".$userProfile->id."\n";
echo "name:     ".$userProfile->name."\n";
echo "password: ".$userProfile->password."\n";
echo "address:  ".$userProfile->address."\n";
 
// Model?
$userProfile->id = 2;
$userProfile->name = 'new name';
$userProfile->password = 'new password';
$userProfile->address = 'new address';
 
var_dump($userProfile->_object);
 
echo "<hr />";
 
$ACL->setRole('admin');
 
$userProfile = $objectProvider->construct('UserProfile', MODE_NOT_STRICT);
//$userProfile->load(1);
 
// View?
echo "id:       ".$userProfile->id."\n";
echo "name:     ".$userProfile->name."\n";
echo "password: ".$userProfile->password."\n";
echo "address:  ".$userProfile->address."\n";
 
// Model?
$userProfile->id = 2;
$userProfile->name = 'new name';
$userProfile->password = 'new password';
$userProfile->address = 'new address';
 
var_dump($userProfile->_object);
 
Simple ACL system and simple actions on a simple object - UserProfile ;)

It's in "silent mode" - that's the mode I'm going to use in future projects.

The load/save methods I've defined are to show that the object itself is not aware of being "wrapped".

Re: Extending PHP method/property modifiers

Posted: Sun Oct 19, 2008 6:40 pm
by VladSun
And some benchmarking :(

Code: Select all

define('TEST_COUNT', 10000);
 
$up->name = 'new name';
$time_start = microtime(true);
for ($i=0; $i<TEST_COUNT; $i++)
    $s = "name:     ".$up->name."\n";
$time_end = microtime(true);
$time1 = $time_end - $time_start;
echo "UserProfile read : $time1 seconds\n";
 
$userProfile->name = 'new name';
$time_start = microtime(true);
for ($i=0; $i<TEST_COUNT; $i++)
    $s = "name:     ".$userProfile->name."\n";
$time_end = microtime(true);
$time2 = $time_end - $time_start;
echo "UserProfile (protected) read : $time2 seconds\n";
 
$time_start = microtime(true);
for ($i=0; $i<TEST_COUNT; $i++)
    $up->name = 'new name';
$time_end = microtime(true);
$time3 = $time_end - $time_start;
echo "UserProfile write : $time3 seconds\n";
 
$time_start = microtime(true);
for ($i=0; $i<TEST_COUNT; $i++)
    $userProfile->name = 'new name';
$time_end = microtime(true);
$time4 = $time_end - $time_start;
echo "UserProfile (protected) write : $time4 seconds\n";
 
echo "UserProfile (protected) / UserProfile [read] : ".($time2 / $time1)."\n";
echo "UserProfile (protected) / UserProfile [write] : ".($time4 / $time3)."\n";
 
Results:
UserProfile read : 0.0058071613311768 seconds
UserProfile (protected) read : 0.036082983016968 seconds
UserProfile write : 0.00496506690979 seconds
UserProfile (protected) write : 0.047213077545166 seconds
UserProfile (protected) / UserProfile [read] : 6.2135320441762
UserProfile (protected) / UserProfile [write] : 9.5090516206483
Pretty bad :(
It's much slower ... not percent, but times slower :(

Re: Extending PHP method/property modifiers

Posted: Sun Oct 19, 2008 6:57 pm
by VladSun
LOL:

Code: Select all

define('TEST_COUNT', 10000);
 
$time_start = microtime(true);
for ($i=0; $i<TEST_COUNT; $i++)
    $up = new UserProfile();
$time_end = microtime(true);
$time11 = $time_end - $time_start;
echo "UserProfile create : $time11 seconds\n";
 
$userProfile->name = 'new name';
$time_start = microtime(true);
for ($i=0; $i<TEST_COUNT; $i++)
    $userProfile = $objectProvider->construct('UserProfile', MODE_NOT_STRICT);
$time_end = microtime(true);
$time12 = $time_end - $time_start;
echo "UserProfile (protected) create : $time12 seconds\n";
 
echo "UserProfile (protected) / UserProfile [create] : ".($time12 / $time11)."\n";
UserProfile create : 0.020024061203003 seconds
UserProfile (protected) create : 1.8288569450378 seconds
UserProfile (protected) / UserProfile [create] : 91.332968197459
BAD!

Re: Extending PHP method/property modifiers

Posted: Thu Oct 23, 2008 8:12 am
by VladSun
Any comments, suggestions, ideas?

Re: Extending PHP method/property modifiers

Posted: Thu Oct 23, 2008 9:02 am
by koen.h
While the speed seems really bad remember that this isn't a real world application you are testing. It does say you are going to lose some performance. However, I always tell myself that if I think I have a killer feature, a performance hit of about .02 seconds is allowed (on my old ubuntu machine, my new vista machine gives worse performance benchmarks actually =).

How many objects do you think need the ACL encapsulation?

Next, I still don't see how this system can get a number of items out of the database without checking access rights after each one is created.

Re: Extending PHP method/property modifiers

Posted: Thu Oct 23, 2008 9:09 am
by VladSun
koen.h wrote:How many objects do you think need the ACL encapsulation?
All the action controllers and all of the subviews/models in the current action controller.
koen.h wrote:Next, I still don't see how this system can get a number of items out of the database without checking access rights after each one is created.
I'm not sure I understand what "items" refer to. Can you give me a pseudo-code example?

Re: Extending PHP method/property modifiers

Posted: Thu Oct 23, 2008 9:16 am
by koen.h
VladSun wrote: All the action controllers and all of the subviews/models in the current action controller.
For a given request that's a couple of controllers (mostly 1) plus a couple of models (also not many) plus a couple of subview (not many) = a couple. Given 10 seconds for 10000 controlled objects this will give about 0.02 seconds extra load for these couple of controlled objects. I know it's not a scientific enquiry but it would encourage me to test my current framework with it.
I'm not sure I understand what "items" refer to. Can you give me a pseudo-code example?
Eg a page with the latest 10 blog posts. Do you have to get them out of the db one by one and check them, or can you do it in one query (eg SELECT * FROM posts WHERE allowed to view)?

Re: Extending PHP method/property modifiers

Posted: Thu Oct 23, 2008 9:29 am
by VladSun
koen.h wrote:
I'm not sure I understand what "items" refer to. Can you give me a pseudo-code example?
Eg a page with the latest 10 blog posts. Do you have to get them out of the db one by one and check them, or can you do it in one query (eg SELECT * FROM posts WHERE allowed to view)?
If I understand you right, it's still beyond the ability of the current implementation. I still can't figure out a implementation of permitions based on a value of a property of an object.

I.e. I still can't define "owner", "group" etc.

In a common situation, the query will be SELECT * FROM posts.
Then a new protected object Post is created for every row. It has a public property "text". If the ACL says: it's not readable and it's not writable then the View (again protected object) associated with object Post, propery "text" will be an instance of NullObject.
It's not very clear yet, but it would be something like this.