2013-05-22 1 views
2

В последнее время я пытаюсь создать свою собственную фреймворк PHP, просто чтобы узнать из него (поскольку мы можем изучить некоторые более крупные и более надежные рамки для производства). Одна из концепций дизайна, которую я имею в настоящее время, заключается в том, что большинство основных классов в основном работают с статическими функциями внутри классов.Альтернативы статическим методам в фреймворке PHP

Теперь несколько дней назад я видел несколько статей о «Статических методах - это смерть для проверки». Это касалось меня как .. да .. мои классы содержат в основном статические методы. Основная причина, по которой я использовал статические методы, состоит в том, что для множества классов никогда не потребуется больше одного экземпляра, а статические методы легко подходить в глобальной области , Теперь я знаю, что статические методы на самом деле не лучший способ сделать что-то, я ищу лучшую альтернативу.

Представьте себе следующий код, чтобы получить элемент конфигурации:

$testcfg = Config::get("test"); // Gets config from "test" 
echo $testcfg->foo; // Would output what "foo" contains ofcourse. 

/* 
* We cache the newly created instance of the "test" config, 
* so if we need to use it again anywhere in the application, 
* the Config::get() method simply returns that instance. 
*/ 

Это пример того, что я в настоящее время. Но, согласно некоторым статьям, это плохо.
Теперь, я мог бы сделать это так, как, например, CodeIgniter делает это, используя:

$testcfg = $this->config->get("test"); 
echo $testcfg->foo; 

Лично я нахожу это труднее читать. Вот почему я предпочел бы другой путь.

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

Обратите внимание, что я ищу какую-то передовую практику или что-то в том числе образец кода, а не некоторые случайные идеи. Кроме того, если я связан с шаблоном стиля метода $ this-> class->, то могу ли я реализовать это эффективно?

+0

Реми: @ Элиас сказал все в своем ответе, но для вашей части -> get() ...рассмотрели ли вы реализацию интерфейса 'ArrayAccess', который затем позволит вам использовать ваш класс, как если бы он был массивом? –

+0

@ SébastienRenauld, на самом деле нет. Я немного читал об этом несколько дней назад, но я думаю, я должен увидеть и попробовать, как это работает. Если бы вы могли кратко рассказать об этом, что именно он делает? – RemiDG

+0

@ Remi-X: Довольно легко: 'класс Foo реализует ArrayAccess' и [читает документы интерфейса] (http://php.net/manual/en/class.arrayaccess.php). Все _listed_ в интерфейсе должно быть реализовано в классе, и вам хорошо идти –

ответ

4

В ответ на комментарии Sébastien Renauld: here's an article on Dependency Injection (DI) and Inversion of Control (IoC) с некоторыми примерами и несколько дополнительных слов по принципу Голливуда (что очень важно при работе над каркасом).

Говорить, что ваши классы никогда не потребуются больше, чем один экземпляр, не означает, что статика обязательна. На самом деле это не так. Если вы просматриваете этот сайт и читаете вопросы PHP, которые касаются Singleton «pattern», вы скоро узнаете, почему одиночные игры - это немного не-нет.

Я не буду вдаваться в подробности, но тестирование и синглеты не смешиваются. Зависимость инъекции, безусловно, стоит поближе. На этот раз я оставлю это.

Чтобы ответить на ваш вопрос:
Ваш exaple (Config::get('test')) означает, что вы имеете статическое свойство в Config классе где-то. Теперь, если вы сделали это, как вы говорите, чтобы облегчить доступ к данным, представьте, какой кошмар было бы для отладки вашего кода, если бы это значение должно было где-то меняться ... Это статично, поэтому измените его один раз, и он меняется везде. Выяснить, где он был изменен, может быть сложнее, чем вы ожидали.Тем не менее, это ничего по сравнению с теми, кто использует ваш код будет в той же ситуации.
И все же проблемы real начнутся только тогда, когда этот человек, использующий ваш код, хочет проверить, что он сделал: если вы хотите получить доступ к экземпляру в заданном объекте, который был создан в некоторых класс, есть много способов сделать это (особенно в рамках):

class Application 
{//base class of your framework 
    private $defaulDB = null; 
    public $env = null; 
    public function __construct($env = 'test') 
    { 
     $this->env = $env; 
    } 
    private function connectDB(PDO $connection = null) 
    { 
     if ($connection === null) 
     { 
      $connection = new PDO();//you know the deal... 
     } 
     $this->defaultDB = $connection; 
    } 
    public function getDB(PDO $conn = null) 
    {//get connection 
     if ($this->defaultDB === null) 
     { 
      $this->connectDB($conn); 
     } 
     return $this->defaultDB; 
    } 
    public function registerController(MyConstroller $controller) 
    {//<== magic! 
     $controller->registerApplication($this); 
     return $this; 
    } 
} 

Как вы можете видеть, Application класс имеет метод, который проходит экземпляр Application к контроллеру, или любой другой частью вашей структуры вы хотите предоставить доступ к области действия класса Application.
Обратите внимание, что я объявлял свойство defaultDB как частную собственность, поэтому я использую getter. Я могу, если захочу, передать связь с этим получателем. Конечно, гораздо больше вы можете сделать с этим соединением, но я не могу беспокоиться о написании полной структуры, чтобы показать вам все, что вы можете здесь сделать :).

В принципе, все контроллеры продлят MyController класса, может быть абстрактным классом, который выглядит следующим образом:

abstract class MyController 
{ 
    private $app = null; 
    protected $db = null; 
    public function __construct(Application $app = null) 
    { 
     if ($app !== null) 
     { 
      return $this->registerApplication($app); 
     } 
    } 
    public function registerApplication(Application $app) 
    { 
     $this->app = $app; 
     return $this; 
    } 
    public function getApplication() 
    { 
     return $this->app; 
    } 
} 

Так что в вашем коде, вы можете легко сделать что-то вдоль линий:

$controller = new MyController($this);//assuming the instance is created in the Application class 
$controller = new MyController(); 
$controller->registerApplication($appInstance); 

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

$controller->getApplication()->getDB(); 

Вы можете протестировать свой фреймворк легко, передав другое соединение БД с методом getDB, если в этом случае свойство defaultDB не было установлено. С некоторой дополнительной работой, вы можете зарегистрировать несколько соединений DB в то же время и получить доступ к тем, по желанию, также:

$controller->getApplication->getDB(new PDO());//pass test connection here... 

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

В ответ на замечания от OP:

О том, как я бы решить Config класс. Честно говоря, я бы в значительной степени сделал то же самое, что и с свойством defaultDB, как показано выше. Но я бы, вероятно, позволит более целенаправленным контроль на какой класс получает доступ к какой части конфигурации:

class Application 
{ 
    private $config = null; 
    public function __construct($env = 'test', $config = null) 
    {//get default config path or use path passed as argument 
     $this->config = new Config(parse_ini_file($config)); 
    } 
    public function registerController(MyController $controller) 
    { 
     $controller->setApplication($this); 
    } 
    public function registerDB(MyDB $wrapper, $connect = true) 
    {//assume MyDB is a wrapper class, that gets the connection data from the config 
     $wrapper->setConfig(new Config($this->config->getSection('DB'))); 
     $this->defaultDB = $wrapper; 
     return $this; 
    } 
} 

class MyController 
{ 
    private $app = null; 
    public function getApplication() 
    { 
     return $this->app; 
    } 
    public function setApplication(Application $app) 
    { 
     $this->app = $app; 
     return $this; 
    } 
    //Optional: 
    public function getConfig() 
    { 
     return $this->app->getConfig(); 
    } 
    public function getDB() 
    { 
     return $this->app->getDB(); 
    } 
} 

Эти последние два метода на самом деле не требуется, вы могли бы точно так же написать что-то вроде:

$controller->getApplication()->getConfig(); 

Опять же, этот фрагмент все немного грязный и неполный, но это действительно идет, чтобы показать вам, что вы можете «выставить» некоторых свойства одного класса, путем передачи ссылки на этот класс к другому. Даже если свойства являются частными, вы можете использовать геттеры для доступа к ним одинаково. Вы также можете использовать различные методы регистрации для контроля того, что зарегистрированный объект разрешен для просмотра, как это было сделано с DB-оберткой в ​​моем фрагменте. Класс DB не должен иметь дело с представлениями, пространствами имен или автозагрузчиками. Вот почему я только регистрирую раздел DB в конфиге.

В основном, многие ваши основные компоненты в конечном итоге будут использовать ряд методов. Другими словами, они в конечном итоге реализуют данный интерфейс. Для каждого основного компонента (при условии классического шаблона MVC) у вас будет один абстрактный базовый класс и цепочка наследования из 1 или 2 уровней дочерних классов: Abstract Controller>DefaultController>ProjectSpecificController.
В то же время, все эти классы, вероятно, ожидают, что другой экземпляр будет передан им при построении. Просто посмотрите на index.php любого проекта ZendFW:

$application = new Zend_Application(APPLICATION_ENV); 
$application->bootstrap()->run(); 

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

public function initController(Request $request) 
{ 
    $this->currentController = $request->getController(); 
    $this->currentController = new $this->currentController($this); 
    return $this->currentController->init($request) 
            ->{$request->getAction().'Action'}(); 
} 

Пропустив $this конструктору класса контроллера, что класс может использовать различные методы получения и установки для получить все, что ему нужно ... Посмотрите на приведенные выше примеры, он может использовать getDB или getConfig и использовать эти данные, если это необходимо.
Вот как большинство фреймворков я возился или работал с функцией: приложение запускается в действие и определяет, что нужно делать. Это принцип Голливуда или инверсия управления: приложение запускается, и приложение определяет, какие классы ему нужны, когда. В приведенной ссылке я полагаю, что это сравнивается с магазином, создающим своих собственных клиентов: магазин построен и решает, что он хочет продать. Чтобы продать его, он создаст клиентов, которых он хочет, и предоставит им средства, необходимые для их покупки ...

И, прежде чем я забуду: Да, все это можно сделать без единого статического переменная, не говоря уже о функции, вступает в игру. Я построил свои собственные рамки, и я никогда не чувствовал, что нет другого пути, кроме «go static». Сначала я использовал шаблон Factory, но быстро отбросил его.
IMHO, хорошая структура модульная: вы должны иметь возможность использовать ее (например, компоненты Symfony), без проблем. Использование Factory pattern делает вас считать слишком много. Вы принимаете класс X, который не является данным.
Регистрация тех классов, которые доступны, позволяет использовать гораздо более портативные компоненты. Рассмотрим это:

class AssumeFactory 
{ 
    private $db = null; 
    public function getDB(PDO $db = null) 
    { 
     if ($db === null) 
     { 
      $config = Factory::getConfig();//assumes Config class 
      $db = new PDO($config->getDBString()); 
     } 
     $this->db = $db; 
     return $this->db; 
    } 
} 

В противоположность:

class RegisteredApplication 
{//assume this is registered to current Application 
    public function getDB(PDO $fallback = null, $setToApplication = false) 
    { 
     if ($this->getApplication()->getDB() === null) 
     {//defensive 
      if ($setToApplication === true && $fallback !== null) 
      { 
       $this->getApplication()->setDB($fallback); 
       return $fallback;//this is current connection 
      } 
      if ($fallback === null && $this->getApplication()->getConfig() !== null) 
      {//if DB is not set @app, check config: 
       $fallback = $this->getApplication()->getConfig()->getSection('DB'); 
       $fallback = new PDO($fallback->connString, $fallback->user, $fallback->pass); 
       return $fallback; 
      } 
      throw new RuntimeException('No DB connection set @app, no fallback'); 
     } 
     if ($setToApplication === true && $fallback !== null) 
     { 
      $this->getApplication()->setDB($fallback); 
     } 
     return $this->getApplication()->getDB(); 
    } 
} 

Хотя последняя версия немного больше работы, чтобы написать, это совершенно ясно, какой из двух является лучшим выбором. Первая версия просто предполагает слишком много, и не позволяет использовать защитные сетки. Это также довольно диктаторский: предположим, что я написал тест, и мне нужны результаты, чтобы перейти к другой БД. Поэтому мне нужно изменить соединение с БД, для всего приложения (ввод пользователя, ошибки, статистика ... все они, вероятно, будут храниться в БД).
Для этих двух причин лучшим вариантом является второй фрагмент: я могу передать другое соединение с БД, которое перезаписывает приложение по умолчанию, или, если я не хочу этого делать, я могу либо использовать соединение по умолчанию, либо попытайтесь создать соединение по умолчанию. Храните соединение, которое я только что сделал, или нет ... выбор полностью мой. Если ничего не работает, я просто получаю на меня бросок RuntimeException, но это не главное.

+1

Это более 1. Возможно, стоит упомянуть IoC вместе с DI, чтобы упростить тестирование, хотя возможно, позволяя использовать одноэлементные подходы. –

+0

Удивительный ответ. Не могли бы вы просто прояснить одно в своем ответе? У меня есть небольшое представление о том, как я буду вводить свой собственный класс в контроллер (так что я могу сделать '$ this-> class-> method()'), но я тоже хотел бы видеть ваш. Кроме того, по мере того, как вы заявляете, что хотите быстро получить ответ, мне бы хотелось увидеть то, что вы оставили сейчас: P – RemiDG

+0

@ Remi-X: я бы не стал _inject_ моим объектом в класс контроллера, я бы расширил рамки класс контроллера, и используйте это как мой контроллер. AFAIK, так работает большинство фреймворков. Таким образом, вам нужно будет только '$ this-> method()' где '$ this' является' class MyCustomController extends MyController'. Если вы передадите этот объект методу или функции, использующим typehinting, он примет объект как экземпляр 'MyController', а также экземпляр' MyCustomController' –

0

Magic methods поможет Вам: увидеть примеры о __get() и __set()

Вы должны также взглянуть на namespaces: это может помочь вам избавиться от некоторых классов с только статическими методами.

+1

Вы хоть представляете, насколько неэффективны эти магические методы? –

+0

@ SébastienRenauld хорошо, это особенность, а не взлом. Поэтому я ожидаю, что эффективность будет справедливой и улучшающейся во времени. –

+0

@ JanTuroň: Хотя некоторые вещи могут улучшиться, они никогда не побьют здравый смысл. Вы можете добавлять свойства к объектам «на лету», но для этого требуется перераспределение памяти для каждого экземпляра. Независимо от того, как вы смотрите на это, это замедлит вас. То же самое касается магических методов: выполнить поиск по экземпляру, затем вызвать метод (который вызывает другие функции или циклы x раз) и вернуть некоторые данные ... это просто дополнительные накладные расходы, вызванные ленивым программированием –

Смежные вопросы