Подмешиваем виагру в коктейли с PHP или незаметные "миксины"

  • написал: MpaK
  • 313
Замешанный летний коктейльПо ряду причин я занимаюсь коммерческой разработкой на PHP, но краем глаза, в свободное время люблю изучать всё новое и всё приятно. Этим ново-приятным для меня последнее время стал Ruby on Rails, скорее давно стал, да как бы не было совсем столько времени, чтобы плотнее заняться этим замечательным инструментом. В последнее время я всё больше стал писать на Ruby в частности, так и с применением этого шикарного фреймворка. Всё меня в нём радует, как синтаксис языка, так и «магия» переданная в самом лучшем виде.

Одной из таких радостный вещей в Ruby так и в Rails для меня стали модули и возможность подмешать их в класс, придав тем самым классу (объекту) дополнительные методы и силы. Тем самым можно как раз имитировать множественное наследование, этот противоречивый термин у которого есть много противников, но и столько же поклонников. Я лично считаю, что всё полезно в языке, что придаёт ему гибкости и пока не начинает путать и мешать.

Но, что в Ruby делается с такой легкостью и заложено на уровень языка в языке PHP заставляет задуматься. Смотрите как красив и лаконичен Ruby исходник:

module Taggable
	def mixinize
		puts "Hi, iam mixinize #{@name}"
	end
end

class News
	include Taggable
	def initialize name
		@name = name
	end
end

news = News.new "News model"
news.mixinize

Красиво, правда?!

На этапе разработки модулей для CMS я понял, что ряд действий в модулях и моделях часто повторяются. Причем иногда приходиться их комбинировать. Например я хочу модель Новости иметь возможность тэггировать и сортировать, а модель Статей только тэггировать, а записи в Блоге только тэггировать.
Конечно, методы можно было бы вынести в в 3 отдельных класса наследовать их методы и работать спокойно наследуя нужный нам один из 3х: Taggable, Sortable, TaggableSortable класс?
Но вы уже сами наверное понимаете как это выглядит некошерно и во многом пришлось бы переобъявлять вызовы в последнем классе для вызовов их из 2х первых, не удобно, очень неудобно.

Тут бы нам на помощь пришло бы множественное наследование, очень бы просто было объявить
class News extends Taggable, Sortable{}

Но увы в языке PHP нет таких возможностей, потому приходиться реализовывать как раз этот самый паттерн MIXINs.

Немного порывшись в сети, найдя ряд хороших и добротных примеров первые реализации уже были. Но был маленький гвоздь не позволявший например из методов подмешанного класса обращаться к свойствам и методам родительского класса (к которому нас подмешали), разумеется у нас не было таких вещей как parent или иного указания на того к кому нас подмешали.

Решение: вы не поверите случайно подумалось и идея нашлась так же в сети — $THIS! Мы всегда можем при подмешивании, давать указание на того к кому нас подмешивают в виде $this, а дальше всё просто, логично + немного магических методов и вот оно такое простое, но вполне доходчивое примерное решение вопроса.

<?php

abstract class Model {
    private
        $mixed = array()
    ;

    public function __get($name){
        foreach($this->mixed as $object)
            if(property_exists($object, $name)) return $object->$name;
        throw new Exception('Property $name is not defined.');
    }

    public function __set($name,$value){
        foreach($this->mixed as $object)
            if(property_exists($object, $name)) return $object->$name = $value;
        throw new Exception('Property $name is not defined.');
    }

    public function __isset($name){
        foreach($this->mixed as $object)
            if(property_exists($object,$name) && isset($this->$name)) return true;
        return false;
    }

    public function __unset($name){
        foreach($this->mixed as $object)
            if(property_exists($object,$name)) $object->$name = null;
    }

    public function __call($name,$parameters){
        foreach($this->mixed as $object)
            if(method_exists($object,$name))
                return call_user_func_array(array($object,$name),$parameters);
        throw new Exception("Method $name is not defined.");
    }

    public function mix($class){
        return $this->mixed[$class] = new $class($this);
    }

    public function __access_get($property){
        if(property_exists($this, $property))    return $this->$property;
            throw new Exception('Property $property is not defined in parent.');
    }

    public function __access_set($property, $value){
        if(property_exists($this, $property))    return $this->$property = $value;
            throw new Exception('Property $property is not defined in parent.');
    }

    public function __access_call($method, $value){
        if(method_exists($this, $method))    return call_user_func_array(array($this, $method), $value);
            throw new Exception("Method $method is not defined in parent.");
    }

}

abstract class Mixin{
    private $parent;

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

    public function __get($property){
        return $this->parent->__access_get($property);
    }

    public function __set($property, $value){
        return $this->parent->__access_set($property, $value);
    }

    public function __call($method, $value){
        return $this->parent->__access_call($method, $value);
    }
}

class News extends Model {
    public $data = array(3,2,4,5,1,6);
}

class Tagable extends Mixin{
    public
        $name = "Tagable",
        $tags = array( '1'=>'php', '2'=>'perl', '3'=>'ruby', '4'=>'python', '5'=>'javascript', '6'=>'c++' )
       ;

    function get_tags(){
        foreach( $this->data as $id ){
            $tags[] = $this->tags[$id];
           }
           $tags = implode (', ', $tags);
        echo "{$this->name}: {$tags}\n";
    }
}

class Sortable extends Mixin{
    public $name = "Sortable";
    function get_sorted(){
        $data = $this->data;
        sort($data);
        $data = implode( ', ', $data );
        echo "{$this->name}: {$data}\n";
    }
}

$news = new News();

$news->mix('Sortable');
$news->mix('Tagable');

$news->get_sorted();
$news->get_tags();


В общем пример простой и думаю понятный даже начинающим изучать PHP5.

Но если будут вопросы рад буду ответить или прокомментировать.

5 комментариев

avatar
За магию руки бы поотрывал :[
avatar
  • MpaK
  • 0
Без магии никак, всё программирование сродни магии! :)))
avatar
Магические get set call — полная х#%ня. И юзать их следует только в случае крайней необходимости.
Как вы потом будете документировать функции и переменные? Вы что будете каждый раз лезть в исходники, если что-то забудете?

Вы с такими вещами как phpDoc и автокомплит знакомы или до сих пор в блокноте пишете?
avatar
  • MpaK
  • 0
А мотивировать или так просто почесать, чем это так не по душе обиды на set call и get?

Документируйте ручки, тот же формат phpDoc это позволяет.
Автокомплит, конечно без указания тех же директив phpDoc срабатывать не будет, но их вполне можно указать.

Не считайте себя умнее других если вы сами на столько не аккуратны со своим кодом.
avatar
так трудно понять, что код от этого получается запутанным и плохо читаемым?!

Как документировать функции и переменные класса, если они могут быть любыми и создаются во время выполнения))

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