использование псевдостатических методов на примере Zend_Db_ActiveRecord

    Случилось так, что осваивая Zend Framework я решил написать компонент Zend_ActiveRecord по функциональности как можно более похожий на Rails. Подобное предложение возникало на комьюнити зенда, но давно не обновлялось, да при этом требовало php 5.3 из-за его __callStatic(). Меня этот факт не устроил, но необходимость вызова динамических методов класса модели как статичных по прежнему остается очень актуальной темой, я же попробовал разобраться с этим как раз на примере моей собственной ActiveRecord для зенда

    Допустим у нас есть модель Post, которая представляет таблицу (либо сервис) записей в блоге.

    class Post extends Zend_ActiveRecord_Abstract {}


    Логичным и удобным было бы производить поиск как

    Post::find('all');


    Тем самым мы избавляемся от необходимости инстанцировать модель каждый раз как нам понадобится доступ к поисковым методам. На первый взгляд кажется что достаточно описать этот метод как статический, но! При вызове статического метода не вызывается конструктор, а именно в нем по логике должны быть реализованы основные функции для приведения модели в рабочее состояние. Например, одним из основных преимуществ паттерна ActiveRecord является целостность и гибкое использование зависимых моделей, i.e. ассоциаций, поэтому попробуем добавить, например, комментарии к постам и произведем поиск всех постов И связанных с ними комментариев. В Rails это бы выглядело так:

    class Post < ActiveRecord::Base
        has_many :comments
    end

    class Comment < ActiveRecord::Base
        belongs_to :post
    end

    Post.find :all, :include=>:comments


    has_many и belongs_to в Rails представляют собой методы регистрации ассоциаций, по аналогии на php это должно выглядеть так:

    class Post extends Zend_ActiveRecord_Abstract {
        // выделяем отдельную функцию для инициализации, т.к. в php
        // нет дефолтного конструктора при статических вызовах
        protected static function initialize() {
            // comments — в множественном числе, как в Rails,
            // этот параметр обрабатывается инфлектором, так что не смущаемся
            self::has_many('comments');
        }
    }

    class Comment extends Zend_ActiveRecord_Abstract {
        protected static function initialize() {
            self::belongs_to('post');
        }
    }

    // производим поиск
    Post::find(array('all','include'=>'comments'));

    Первое что бросается в глаза, так это то что мы должны использовать статические методы для объявления ассоциаций. Это необходимо делать потому, что на этапе вызова статического find у нас нет экземпляра класса, и вызов инициалайзера должен производится проводится вручную из find() а не из конструктора по умолчанию, что согласитесь неудобно, особенно если в теле инициализации использовать другие функции, которые по умолчанию подразумевают наличие экземпляра класса. Например

    class Post extends Zend_ActiveRecord_Abstract {
        function initialize{
            self::has_many('comments');
        
            // для примера поставим дату инициализации объекта
            $this->set_attribute('initialized_at',timestamp());

        }
    }


    При статическом вызове это вызовет fatal error.

    Существуют также ситуации когда нужно вызывать find из уже созданного экземпляра класса

    // подгузка поста с id=1
    $post = new Post(1);

    // вызываем поиск чего-нибудь
    $something = $post->find(array('all','conditions'=>array(......));

    // либо подгрузка комментариев к посту, вызывается метод find модели Comment, уже созданной по умолчанию
    $otherthing = $post->comments->find(array('all'));

    Все это приводит к тому что в разные моменты жизненного цикла модели необходимо знать как был осуществлен вызов метода, как статический либо как то иначе. Выход был найден: подразумевать метод find как НЕ СТАТИЧЕСКИЙ по умолчанию, что не помешает нам вызывать его статически, а при статическом вызове создавать временный экземпляр класса. PHP сам подсказал удобный способ в одну строчку.

    class Zend_ActiveRecord_Abstract {

        public function find($options = array()) {
            // определим тип вызова по наличию $this. В статическом контексте переменная не определена
            if (!isset($this)) $This = new self(); else $This = $this; // $this и $This — это разные переменные
            // далее работаем с объектом не думая о последствиях, но используя $This вместо $this
            $This->mehod();
            …
        }
    }


    либо можно использовать обвязку

    class Zend_ActiveRecord_Abstract {
        public static funtion get_instance() {
            $obj = new self();
            return $obj;
        }

        public funtion find($options = array()) {
            if (!isset($this)) return self::get_instance()->find($options);
            // далее работаем как обычно
        }

    }


    При таком вызове мы просто эмулируем в самом псевдостатическом методе действия, которые было бы неудобно использовать в остальном коде. Т.е. в данном случае вызов

    $post_instance = new Post();
    $post_instance->find();

    и
    Post::find();


    абсолютно идентичны, но в последнем случае нам не требуется каждый раз производить создание экземпляра класса

    Похожие публикации

    Средняя зарплата в IT

    120 000 ₽/мес.
    Средняя зарплата по всем IT-специализациям на основании 5 897 анкет, за 1-ое пол. 2021 года Узнать свою зарплату
    Реклама
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее

    Комментарии 13

      0
      Хм. А разве префикс имен классов "Zend" не зарезервирован разработчиками?
        0
        может быть и так, но используя Zend_Loader хочется придерживаться общего вида
          0
          В таком случае, как вариант, можно было бы создать рядом с директорией Zend свою, назвать ее, например, OmeZ, запихнуть обе в директорию library и записать путь к последней в include_path. Тогда Zend_Loader будет работать и с именами OmeZ_ActiveRecord. Т.е. по названному классическим макетом в соответствующей статье.
            0
            да, я это прекрасно знаю. Но тем не менее я не вижу существенной разницы в определении пространства имен, тем более для открытых фреймворков. Да и тема не об этом
        0
        Я лишними обвесами
        if (!isset($this)) return self::get_instance()->find($options);
        не пользуюсь, а к псевдостатитке обращаюсь так:

        Вместо:
        $post_instance = new Post();
        $post_instance->find();

        *****
        Post::getInstance()->find();
          0
          да, я согласен, но есть несколько нюансов почему именно так: в вызовах мы намерянно используем статичный вызов, потому как де-факто он и должен быть статичным, стопором являются лишь ограничения языка. В дальнейшем это дает гибкую расширяемость и миграцию по версиям. К примеру выйдет наконец 5.3 версия PHP, Post::getInstance()->find() будет уже не кстати. Обвязка на то и обвязка, что делается ориентация на дальнейшее развитие компоненты без реструктуризации остального кода. Post::find() таким и останется везде.
          Да причем такая форма вызова красивей, да и работать будет быстрее кстати. Я точно не знаю почему, вроде делается один лишний вызов функции, но я проверил по бенчу, и там мой метод выдал на 7% большую производительность чем вариант Post::getInstance()->find()
            +1
            Не знаю откуда там взялось 7% плюса. Post::getInstance()->find() на одну операцию меньше выполняет:
            (!isset($this)) не исполняется, а isset на неопределенных переменных медленней всего работает.

            А с 5.3 всё равно переписывать прийдётся (Только любителям чистого кода). В первом случае убирать в модели обвес, во втором случае в Контроллере. Где быстрее — зависит от приложения. Правда в большинстве случаев Модель переписывать быстрее.
              0
              я тоже не знаю откуда 7%, но факт. Бенч делался на 10.000 итераций по обоим методам. Хотя по большому счету эти операции не так уж часто будут встречаться, т.к. find() делается один раз на запрос, все связанные модели выдираются через генераторы ассоциаций на более низком уровне. А вместе с php-акселлераторами этот вопрос вообще отпадает.

              ну, с 5.3 переписать только несколько базовых функций в модели и это все что потребуется, а в контроллерах капаться это намного дольше, да и все равно в модель лезть придется в любом случае.

              ЗЫ. "чистый код - залог психического здоровья"
                0
                Да ну его, getInstance() наглядней, без "особой" уличной магии :)

                Но это уже вопрос идеологии.
                  0
                  кстати по поводу уличной магии - жди статью про замыкания в контексте PHP, занимательная штука для тех кто когда-либо писал на руби
            0
            да, и чуть не забыл, вызов как статичной функции дает возможность впоследствии использовать паттерн ActiveHandler для моделей, что тоже дает свои плюсы
              0
              Гугль на слова ActiveHandler pattern PHP, чушь выдаёт.
              В чом соль этого паттерна?
                0
                ActiveHandler - это обыкновенный handler для ORM. может он еще как-то по другому называется, но я знаком с этим названием еще со старых версий ROR. Суть в том что существует 2 разных класса, один используется для "готового" объекта, другой для обработки еще не существующего, либо в роли "посредника" между данными и процедурной частью. Управление происходит назначением getters и setters. Например у есть уже загруженая из базы запись Post:
                $post = new Post(1);
                но для нее не определено свойство $post->comments, т.к. я еще не загружал эту связь и также не указал ее для автоматической загрузки. Тем более по факту это должен быть массив, исходя из типа звязи has_many (one-to-many relationship).
                Допустим мне надо сделать поиск всех комментариев для ЭТОГО поста с каким-нибудь условием. в стиле rails это выглядит так
                $post->comments->find(array('all','conditions'=>......));
                так вот Handler и выполняет эти операции на уровне класса Comment а не Post, тем самым упрощая логику вызова. В рельсах для этого есть динамическая перегрузка методов массива (в руби все есть объект, даже массивы и числа - это все объекты), в пхп для этого я использую handler, а в качестве управляющих структур - методы __get и __set

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

          Самое читаемое