Организация логики приложения

s@nch0

Хочу всё знать
Помогите составить скелет для абстрактного web-приложения.

Есть некий набор классов:
1. класс для работы со списком пользователей: удаление, редактирование, добавление, обновление и т.д.
2. класс для работы объектом "улица": удаление, редактирование, добавление, обновление и т.д.
3. класс для работы объектом "дом": удаление, редактирование, добавление, обновление и т.д.
4. класс для работы объектом "квартира": удаление, редактирование, добавление, обновление и т.д.
...

В каждом из этих классов требуется работа с базой данных. Класс для работы с базой данных имеется - DB.

Как организовать наследование так, чтоб создать экземпляр базы данных только один раз (т.е. подключиться только один раз при инициализации приложения) и использовать его во всех классах при необходимости ?

Передавать экземпляр класса DB в конструктор классов 1-4 - имхо какой-то не очень кошерный вариант. Использовать global в методах классов 1-4 - ещё хуже.

Нужен изящный вариант, красивый :)
Спасибо!
 

s@nch0

Хочу всё знать
Не знаю, честно говоря. Как-то на подсознательном уровне этот вариант мне не нравится... Да и автокомплит в IDE (PhpStorm) при таком варианте не работает в классах 1-4 . :(
 

radioheaded

PHP нуб
Не знаю, честно говоря. Как-то на подсознательном уровне этот вариант мне не нравится... Да и автокомплит в IDE (PhpStorm) при таком варианте не работает в классах 1-4 . :(
Неработающий автокомплит в IDE — отличный аргумент при разработке архитектуры классов. Откройте для себя type hinting и phpdoc, что ли.
Передавать класс соединения в класс работы с конкретной базой и таблицей — это хороший подход. Вы, конечно, можете использовать старый добрый синглтон, но с точки зрения автотестирования первый подход лучше.
 

ksnk

прохожий
конструктор классов 1-4 - имхо какой-то не очень кошерный вариант
Некошерным тут будет то, что тот, кто вызывает классы должен знать структуру вызова конструктора. То есть, если классам, в процессе их развития, внезапно понадобится система кэширования, подсистема работы с сессиями, класс для отправки сообщений почтой, все это тоже придется передавать параметрами конструктора?
Старый добрый синглтон тоже не так практичен, его довольно непросто наследовать и, таким образом, тестировать.
Выход может быть где-то посередине. Достаточно иметь ОДИН синглтон - скелетный класс проекта, через который добираться до всех дополнительных сущностей системы.
что-то вроде ENGINE::db() - дай проектную датабазу, ENGINE::cache() - кэшируй и т.д.
Проблема тестирования самого ENGINE останется, но зато он будет только один и оттестировать его будет проще.
 

AmdY

Пью пиво
Команда форума
ksnk
на самом деле даже проблемы с тестированием нет, если делять не прямой вызов, а через __callStatic
ENGINE::map('db', $dbMock);
ENGINE::db();
 

radioheaded

PHP нуб
Если так рассуждать, то можно писать как угодно, ведь есть runkit, чего париться.
 

s@nch0

Хочу всё знать
Некошерным тут будет то, что тот, кто вызывает классы должен знать структуру вызова конструктора. То есть, если классам, в процессе их развития, внезапно понадобится система кэширования, подсистема работы с сессиями, класс для отправки сообщений почтой, все это тоже придется передавать параметрами конструктора?
Старый добрый синглтон тоже не так практичен, его довольно непросто наследовать и, таким образом, тестировать.
Выход может быть где-то посередине. Достаточно иметь ОДИН синглтон - скелетный класс проекта, через который добираться до всех дополнительных сущностей системы.
что-то вроде ENGINE::db() - дай проектную датабазу, ENGINE::cache() - кэшируй и т.д.
Проблема тестирования самого ENGINE останется, но зато он будет только один и оттестировать его будет проще.
Да, спасибо. Так и начал реализовывать задачу. :)
 

s@nch0

Хочу всё знать
Млин, что-то не идёт дело. :(
Ранее имел дело только с паттерном MVC, обо всех остальных только читал.

Класс для работы с БД сделать как синглтон не проблема. А вот это

иметь ОДИН синглтон - скелетный класс проекта, через который добираться до всех дополнительных сущностей системы.
не могу допереть, как правильно и изящно сделать. Помогите плз.
 

ksnk

прохожий
За примером использования и тонкостей "скелетной" организации приложения разумнее посмотреть на Yii. Они стартовали на 5.2, у которого не было __callStatic, так что по форме вызовы несколько другие, но по сути - то же самое.

Далее - очень схематично моя реализация. Она писалась с оглядкой на Yii, но "своими словами" ;)

Системная конфигурация. Все параметры конфигурации доступны по имени с помощью функции ENGINE:: option($name,$default=''). Устанавливаются ENGINE::set_option($name,$value, $options=null). Системные параметры хранятся в разных местах, в частности, в одном или нескольких php-конфигах. Примерно такого вида
PHP:
<?php
/**
 * основной файл конфигурации системы.
 * При вызове админки производится доопределение файла конфигурации дополнительными опциями
 */
return array(
    /**
     * database options
     */
    'database.host' => 'localhost',
    'database.user' => 'root',
    'database.password' => '',
    'database.base' => 'cms',
    'database.prefix' => 'tmp',

    /**
     * переопределение ключевых объектов системы.
     */
    'engine.aliaces' => array(
        'Main' => 'xAdmin',
        'User' => 'xUser',
        'Sitemap' => 'xSitemap',
        'Database' => 'xDatabaseWithMemcache',
        'Install' => 'installManager',
        'Rights' => 'xRights',
    ),

    /**
     * список потенциальных внешних интерфейсов
     */
    'engine.interfaces' => array(
        'user_find' => 'User::user_find',
        'link' => array('ENGINE_router', 'link'),
        'log' => 'ENGINE_logger::log',
        'template' => array('Main', 'template'),
        'db' => array('Database', 'getInstance'),
        'run' => array('Main', 'run'),
        'action' => array('ENGINE_action', 'action'),
        'has_rights' => array('Rights', 'has_rights'),
    ),
...
Реальный вид файла не такой красивый, он пересохраняется при изменениии параметров в процессе настройки системы, при этом коментарии теряются, но редактировать вручную его все равно можно.

функция ENGINE::getObj($sim_name) - "фабричный" метод, который по символическому имени инициирует, если его еще не было и выдает наружу уже готовый объект-плагин. Плагин - объекты достаточно произвольного вида, с конструктором с пустыми параметрами. Все параметры, нужные для жизни, объекты забирают с помощью ENGINE:: option.
Соответствие символических имен реальным именам классов задается в секции 'engine.aliaces'

функция ENGINE::exec($callable,$arguments). Если is_callable($callable) то просто вызываем его с помощью call_user_func_array. Если $callable - массив, то 0-й элемент массива заменяется на ENGINE::getObj($callable[0]). Если в результате получился is_callablу - вызываем, иначе ругаеся.
PHP:
/**
     * вызвать класс-метод.
     * Если класс из массива - вызывать объект из фабрики.
     *
     * @param callable $func
     * @param null|array $args
     * @param string $error_rule
     * @return mixed
     */
    static function exec(&$func, $args = array(), $error_rule = '')
    {
        if(empty($func)){
            ENGINE::error('callable not defined yet','',$error_rule);
            return null;
        }
        if (is_array($func) && is_string($func[0])) {
            $func[0] = self::getObj($func[0]);
        }
        if (is_callable($func)) 
            return call_user_func_array($func, &$args);
        if (is_array($func))
                ENGINE::error('unresolved callable {{class}}->{{method}}', array('{{class}}' => $func[0], '{{method}}' => $func[1]), $error_rule);
        else
            ENGINE::error('unresolved callable {{function}}', array('{{class}}' => $func), $error_rule);
        return null;
    }
секция 'engine.interfaces' системных параметров определяет набор вызываемых функций. Все элементы, фактически, тупо копируются в приватный массив self::$interface

Ну а сам вызов совершенно прост
PHP:
static public function __callStatic($method, $args)
    {
        return self::exec(self::$interface[$method], $args,array('method'=>$method));
    }
В итоге, встретив первый раз ENGINE::db() мы получим
-- строчка 'Database'=>'xDatabase' массива self::$aliases заменится на 'Database'=>new xDatabaseWithMemcache; Стартует конструктор, база инициализируется.
-- строчка 'db'=>array('Database', 'getInstance'), массива self::$interface заменится на array(&self::$aliaces['Database'],'getInstance')
-- в итоге будет вызван метод &self::$aliaces['Database']->getInstance и его результат будет возвращен.

В следующий раз мы сразу увидим, что запись в таблице self::$interface вполне себе is_callable и сразу вызовем нужный метод.

В моей версии функция getInstance от Database просто служит для получения $this.
 

s@nch0

Хочу всё знать
Спасибо! Идея очень понравилась.

Но сам лично решил остановиться на классической реализации паттерна Registry. Все же своя идея как-то ближе и приятнее в реализации. :) Заодно отшлифую на практике то, о чём только читал...
 

Василий М.

Новичок
Передавать экземпляр класса DB в конструктор классов 1-4 - имхо какой-то не очень кошерный вариант!
самый правильный вариант
создай строитель твоих классов, который будет получать экземпляр с уже впрыснутым объектом БД
 

ksnk

прохожий
Василий М. В общем-то вопрос изначально ставился именно о механизме впрыска. В конструкторе от папочки или из воздуха по мере надобности. Если есть еще какой вариант - почему бы и не предложить?
 

Redjik

Джедай-мастер
Вот так с легкой подачи завсегдатаев DI стал "впрыском" :D
 
Сверху