Независимо перегружаемые свойства

PHP*
Стандартный механизм перегрузки свойств через методы __get и __set весьма не удобен для практического использования, однако с помощью него можно создать удобный dsl для работы со свойствами. Сразу же пример использования (тут и далее используется паттерн адаптивной типизации, с которым рекомендуется предваритильно ознакомиться):

class Title extends ProtoObject {
    protected $_text= '';
    function set_text( $val ){
        return $this->aTitleString( $val );
    }
    function get_text( $val ){
        if( empty( $val ) ) return '[untitled]';
        return $val;
    }

    function aTitleString( $val ){
        aString( &$val );
        if( strlen( $val ) > 255 ) $val= substr( $val, 0, 252 ) . '...';
        return $val;
    }
}

$title= new Title;
$title->text= 123;
var_dump( $title->text ); // string(3) "123"
var_dump( $title->text( '' )->text() ); // string(10) "[untitled]"
echo $title;
// Title Object
// (
//     [_text:protected] =>
// )


Архитектура


Имя поля должно начинаться с подчёркивания, а акцессоры должны иметь соответствующие префиксы. Если вы не указываете специфичный для свойства геттер и/или сеттер, то будут использованы общие get_ и set_. Интерфейс акцессоров прост: они преобразуют переданное им значение. В сеттер передаётся новое значение и результат сохраняется в поле. В геттер на вход поступает сохранённое значение и результат выдаётся во вне. Фактически акцессоры выступают в роли пре- и пост- фильтров.

К свойству можно обращаться не только как к полю, но и как к полиморфной функции. Если ей не передавать аргументов, то она работает как геттер, если передать один параметр — как сеттер поддерживающий «цепочки». Если передать больше параметров — ждите беды ;-) Для перехвата вызовов неизвестных методов, имена которых не совпадают с именами свойств, можно перегрузить метод _call, который по умолчанию просто бросает исключение.

Маленькая плюшка — преобразование объекта в строку по умолчанию делает его дамп с помощью print_r. Вообще, рекомендация по поводу __toString такая, что сей метод должен вызвращать строку наиболее полно отражающую внутреннее состояние объекта.

Ещё несколько примеров свойств


    protected $_count= 0;
    function set_count( $val ){
        $this->_message= null;
        return anUnsignedInt( $val );
    }
Хранит некоторое неотрицательное целое значение. Значения вида '-2' и 2.1 будут преобразованы к 2. Если же передать булево значение или, например, строку с буквами, то будет возбуждено исключение. Реализовать тайпкастер anUnsignedInt предлагаю самостоятельно.

    protected $_message;
    function set_message( $val ){
        throw new Exception( 'message is autogenerated property' );
    }
    function get_message( $val ){
        if( empty( $val ) ) $val= $this->_message= $this->title . ': ' . $this->count;
        return $val;
    }
Ленивое свойство. Вручную установить значение нельзя, так как оно является функцией от значений других полей. Однако, это свойство кэширует в себе вычисленное значение, поэтому в сеттерах полей, от которых оно зависит, следует прописать сброс кэша.

    protected $_point;
    function set_point( $val ){
        return Point::anInstance( $val );
    }
    function get_point( $val ){
        return clone $val;
    }
Сохраняет в себе объект с некоторым интерфейсом. Если передан не объект, то инстанцирует Point с передачей ему соответствующих параметров. Если параметры не верны — исключение. При чтении свойства — возвращается лишь клон, а сохраённый объект остаётся в целости и приватности. Реализацию тайпкастера оставим в качестве домашнего задания ;-)

Ну и на закуску — класс «типизированная переменная»:
class Vary extends ProtoObject {

    protected $_type;
    function set_type( $type ){
        aString( &$type );
        if( !function_exists( $type ) ) throw new Exception( 'unknown type' );
        if( $this->type ):
            $this->_type= $type;
            $this->val= $this->val;
        endif;
        return $type;
    }

    protected $_val;
    function set_val( $val ){
        return $val= call_user_func( $this->type, $val );
    }

    function __construct( $type ){
        $this->type= $type;
    }

    function __toString( ){
        return aString( $this->val );
    }
}

$count= new Vary( aString );
$count->val= '5cm per second';
echo $count->type; // aString
var_dump( $count->val ); // string(14) "5cm per second"
$count->type= aSoftNumber;
var_dump( $count->val ); // int(5)
echo $count; // 5
echo $count->val( '' ); // 0
var_dump( $count );
// object(Vary)#1 (2) {
//  ["_type:protected"]=>
//  string(11) "aSoftNumber"
//  ["_val:protected"]=>
//  int(0)
// }


Собственно виновник торжества


class ProtoObject {

static $version= 8;
static $description= 'common object extension';
static $license= 'public domain';

function __toString( ){
    return print_r( $this, true );
}

function __set( $name, $value= null ){
    $this->_aPropertyName( &$name );
    $method= 'set' . $name;
    if( !method_exists( $this, $method ) ) $method= 'set_';
    $value= $this->{ $method }( $value );
    $this->{ $name }= $value;
    return $this;
}
function set_( $val ){
    return $val;
}

function __get( $name ){
    $this->_aPropertyName( &$name );
    $method= 'get' . $name;
    if( !method_exists( $this, $method ) ) $method= 'get_';
    $value= $this->{ $name };
    $value= $this->{ $method }( $value );
    return $value;
}
function get_( $val ){
    return $val;
}

function __call( $name, $args ){
    try {
        $this->_aPropertyName( &$name );
    } catch( Exception $e ){
        return $this->_call( $name, $args );
    }
    switch( count( $args ) ){
        case 0: return $this->__get( $name );
        case 1: return $this->__set( $name, $args[0] );
        default: throw new Exception( 'wrong parameters count' );
    }
}
function _call( $name, $args ){
    throw new Exception( 'method not found' );
}

function _aPropertyName( $val ){
    if( $val[0] !== '_' ) $val= '_' . $val;
    if( !property_exists( $this, $val ) ) throw new Exception( 'property not found' );
    return $val;
}

}
–14
2 июня 2010, 11:26
6
tenshi –27,1

комментарии (16)

0
l2k #
Статья интерестная (в плане реализации), но совсем не понимаю, где в PHP программе может понадобиться строгая типизация.

Если мне нужен string, я легко использую (string)$obj.

Может вам стоит посмотреть на другие языки программирования, которые имеют нормальную типизацию и начать писать на них вашу задачу?

//p.s. у вас в коде
if( $this->type ):
$this->_type= $type;
$this->val= $this->val;
endif; /// Хмммм, PHP 3? :)
0
tenshi #
она не строгая — она адаптивная. программист сам решает где ему нужна строгая, а где мягкая, а также какой способ приведения типов больше подходит под решаемую задачу (aTitleString, например)

мне не нужна «нормальная» типизация. я хочу сам конструировать типы и правила их приведения

этот синтаксис более лаконичен
0
tenshi #
впрочем, статья не о типизации вообще ._.
0
tenshi #
а вот зачем нужна строгая типизация — написано в статье про адаптивную типизацию, в самом начале.
строгая типизация удобней в отладке. мягкая — в написании.
0
tenshi #
судя по минусам я написал что-то ужасное х)
+2
beylbom #
Вы занимаетесь хуйней
0
tenshi #
м… какой содержательный ответ. что ещё интересного скажешь?
+1
beylbom #
Хы, а зачем мне говорить? Вы и сами тут прекрасно общаетесь в комментах, сам с собой)))))
0
tenshi #
ну, что-то же тебя сподвигло написать столь потрясающее наблюдение…
+1
mrmot #
Вы занимаетесь хуйней! — Это если не дошло с первого раза.
А лично от Вас, мне хотелось бы понять, для чего вообще нужны так называемые геттеры и сеттеры(да, кстати, это паттерн проектирования), какая их роль в проектировании класса?
0
tenshi #
чтобы контролировать работу стороннего кода с полями объекта
0
mrmot #
Как контролировать и какие сторонние объекты?
а методы типа getProperty/setProperty — нужны для того, что бы класс имел унифицированный интерфейс для работы со свойствами. И когда в будущем Ваш маленький класс, где сначала свойства были примитивами, а теперь стали полноценными объектами с инициализацией или надо будет добавить кеширование для свойства — тогда вы просто измените метод get — и получите что хотели, но при этом сама система страдать не будет.
[php]
getProperty() {
return $this->property;
}

getProperty() {
return $this->property->getCalculatedValue();
}
[/php]

И скажу Вам на будущие, такие методы лучше всего иметь реализованными на прямую, а не через __call или __get/__set. код потом проще читать и сопровождать.

Пользуйтесь этими знаниями на здоровье.
+1
tenshi #
ты действительно думаешь, что такой интерфейс:
$obj->setChildsCount( $obj->getChildsCount() + 5 );

лучше такого?
$obj->setChildsCount+= 5;

а моё решение позволяет сначала предоставить прямой доступ к полю, а когда потребуется это изменить — просто добавить нужные методы и они будут вызваны вместо прямой записи данных. интерфейс не поменяется и останется столь же удобным как и был.
0
mrmot #
У меня просто не будет функции setChildsCount().
А вот почему так будет, уже придётся думать самому.
+1
tenshi #
а что будет? increaseChildsCount, decreaseChildsCount, multiplyChildsCount, divideChildsCount..?
–1
tenshi #
$obj->childsCount+= 5;
разумеется

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.