0,0
рейтинг
30 ноября 2012 в 14:24

Разработка → Нативный MVC для Silex PHP Framework из песочницы

После прочтения статьи, рассказывающей как модифицировать микро-фреймворк Silex под архитектуру MVC, у меня возникло двойственное впечатление. Способ имеет право на жизнь, однако:

  1. в проекте не всегда нужна ORM, хочется иметь и простую реализацию Модели;
  2. в Silex уже есть (хотя и не совсем явные) нативные контроллеры;
  3. писать свои автозагрузчики, когда есть возможность добавить нужное в Composer — не есть хорошо.

Давайте посмотрим, что можно сделать.

Будем придерживаться следующей структуры приложения:
+ project
|   + protected
|       composer.json
|       composer.phar
|       composer.lock
|       + app
|           + Controllers
|           + Models
|           + Views
|       + vendor
|       + providers
|           + Providers
|   + public
|       .htaccess
|       index.php
|       + css
|       + img
|       + js

Я настроил виртуальный хост на папку project/public, поэтому .htaccess будет один
# project/public/.htaccess
<IfModule mod_rewrite.c>
    Options -MultiViews
    RewriteEngine On
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteRule ^ index.php [L]
</IfModule>

Устанавливаем Silex с помощью менеджера пакетов Composer


С недавних пор Silex поддерживает такой способ установки и вот почему нужно пользоваться им:
  • возможность одной командой обновить все библиотеки до актуальных или необходимых версий;
  • генерация одного автозагрузчика для всего необходимого.

Создадим файл project/protected/composer.json примерно такого содержания (в зависимости от того, что необходимо вам):

{
    "require":{
        "silex/silex":"1.0.*"
    },
    "minimum-stability": "dev",
    "autoload":{
        "psr-0":{
            "Providers" : "",
            "Controllers" : "app/"
        }
    }
}

Здесь мы хотим получить сам Silex и указываем, что наших Провайдеров (о них чуть дальше) будем загружать из project/protected/Providers, а Контроллеры из project/protected/app/Controllers.
Набор команд для установки:

cd /path/to/project/protected
curl -s http://getcomposer.org/installer | php
php composer.phar install


Silex Providers


Providers — замечательная возможность Silex, позволяющая внедрить стороннюю функциональность. Провайдеры бывают двух типов: ControllerProviderInterface для контроллеров и ServiceProviderInterface для всего остального (в нашем случае — для Модели).

Модель


Напишем простой Service Provider и загрузчик моделей.

<?php
// project/protected/Providers/ModelsServiceProvider.php

namespace Providers;

use Silex\Application;
use Silex\ServiceProviderInterface;

class ModelsServiceProvider implements ServiceProviderInterface {
    public function register(Application $app) {
        $app['models.path'] = array();
        $app['models']      = $app->share(function($app) {
            return new Models($app);
        });
    }
    public function boot(Application $app) {

    }
}
class Models {
    private $app;
    public function __construct(Application $app) {
        $this->app = $app;
    }
    public function load($modelName, $modelMethod, $data = array()) {
        require_once $this->app['models.path'] . $modelName . '.php';

        $Model = new $modelName($this->app);

        return $Model->$modelMethod($data);
    }

}

С Моделью, в общем-то, всё. Теперь мы можем загрузить любую, находящуюся в директории project/protected/app/Models с помощью конструкции $app['models']->load('Class', 'Method', $data) с возможностью передать в нее нужные данные $data. Осталось лишь зарегистрировать нашего провайдера в Silex.

Контроллер


Единственное, о чем нам непосредственно нужно позаботиться, так это об автозагрузке классов контроллеров, но это за нас уже сделал Composer. Так что теперь мы можем подключать контроллеры стандартным методом Silex mount. Посмотрим, как будет выглядеть файл index.php, простейший Контроллер и Модель.

index.php
<?php // project/public/index.php

require_once __DIR__ . '/../protected/vendor/autoload.php';

$app = new Silex\Application();

$app->register(new Providers\ModelsServiceProvider(), array(
    'models.path' => __DIR__ . '/../protected/app/models/'
));

$app->mount('/', new Controllers\Index());

$app->run();


Контроллер
<?php // project/protected/Controllers/Index.php

namespace Controllers;

use Silex\Application;
use Silex\Route;
use Silex\ControllerProviderInterface;
use Silex\ControllerCollection;

class Index implements ControllerProviderInterface {

    public function connect(Application $app) {
        $index = new ControllerCollection(new Route());

        $index->get('/', function() use ($app) {
            $label = $app['models']->load('Pages', 'index');

            return $label;
        });

        $index->get('/{name}', function($name) use ($app) {
            $name = $app['models']->load('Pages', 'hello', $name);

            return "Hello{$name}";
        });

        return $index;
    }
}

Кратко по коду. Метод connect() говорит Silex, что роуты, описанные внутри, надо обрабатывать как часть контроллера, который мы замаунтили в index.php (в данном случае базовым URL для этого контроллера является корень приложения — /). Далее создается переменная $index, она представляет собой что-то вроде частички $app и имеет только функции роутинга. Сами роуты пишутся как обычно.

Модель
<?php // project/protected/Models/Pages.php

class Pages {

    public function index() {
        return "Index";
    }

    public function hello($name) {
        return ", {$name}!";
    }

}


Что в итоге? Простейшая реализация MVC, с возможностью в любой момент начать писать в стандартном для Silex стиле. В данном контексте не рассматривалось Представление, так как уже есть множество готовых решений, из который лично я предпочитаю использовать Twig, благо интеграция с Silex у него 100%.
Весь проект доступен на github.
Дмитрий Харитонов @geakstr
карма
12,0
рейтинг 0,0
Разработчик
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (15)

  • +2
    Routes лучше конфигурировать отдельно. В контроллер инжектнуть request, response и app на случай если надо сделать редирект или бросить исключение. А сами контролеры делать через callable фабрику в pimple. Получается очень даже секси
    • +1
      Почему лучше отдельно? Кроме как «так привычнее».
      • 0
        Просто логичнее редактировать внешний файл, чем класс если нужно добавить или поменять что-то
  • +1
    Что-то вы намудрили с моделями. Почему просто не прописать в секцию «autoload» путь к вашим моделям?

    Либо сделать ещё проще:

        "autoload": {
            "psr-0": { "App": "src/" }
        },
    

    Тогда создание модели сводится к:

    $person = new \App\Model\Person();
    

    И зачем передавать в модель весь контейнер $app? Лучше явно передавать все зависимости:

    $person = new \App\Model\Person($app['db']);
    

    Если у вас много моделей, можно сделать фабрику:

    $app['models_factory'] = $app->protect(function ($modelName) {
        $fqcn = '\\App\\Model\\'.$modelName;
        return new $fqcn($app['db']);
    });
    

    Вызов:

    $preson = $app['models_factory']('Person');
    

    Хотя я предпочитаю явно всё прописывать, это позволяет «шарить» созданные объекты:

    $app['repository.organization'] = $app->share(function() use ($app) {
        return new \App\Repository\OrganizationRepository($app['db']);
    });
    
    $app['repository.person'] = $app->share(function() use ($app) {
        return new \App\Repository\PersonRepository($app['db']);
    });
    
    ...
    


    P.S. Создавать ControllerCollection проще через фабрику, как рекомендуется в документации:

    $controllers  = $app['controllers_factory'];
    
    • 0
      Заставили задуматься. Ваш вариант сильно проще и естественнее. Единственное, если понадобятся какие-то настройки для моделей (помимо указания пути к ним), как по мне, провайдер будет уместнее. Спасибо за совет, люблю простоту и ваш способ мне нравится.

      P.S. Да, знаю, как написано в документации, но мой PhpStorm не хотел «разворачивать» вызов фабрики и предупреждал, что переменная не инициализирована, поэтому с тех пор пишу длинное "new ControllerCollection(new Route());". Понимаю, что можно закрыть глаза и это совершенно не критично — лишь дело привычки.
      • +1
        Вариант для PhpStorm ;)

        /** @var $controllers \Silex\ControllerCollection */
        $controllers = $app['controllers_factory'];
        
        • 0
          Вот же ж, век живи — век учись :-)
  • 0
    Боюсь ваш composer.json не установит ничего, а выдаст ошибку: The requested package silex/silex could not be found in any version…

    Нужно добавить инструкцию «minimum-stability»:«dev» или добавить суффикс dev в строке «silex/silex»:«1.0.*dev»
    • 0
      Я столкнулся с такой же проблемой, как и парсер в вашем комментарии — он «умно» подставляет соответствующего пользователя с хабра. Забыл, что нельзя просто убрать «dev». Сейчас поправлю статью.
  • 0
    А не проще ли просто заюзать Symfony2 если Ваш проект выходит за рамки выдачи 4-5 страничек? Там уже это все уже как-то структурировано и разложено по палочкам, к тому же DI имеется?)
    • 0
      Тут дело вкуса: либо отпиливать ненужное, либо допиливать необходимое. Я еще молодой, не написался велосипедов, поэтому второй подход меня радует больше :)
    • 0
      О чем вы? Сам Silex по-сути и есть DIC
    • 0
      Symfony заюзать как раз совсем не проще :) Вопрос целесообразности, для многих проектов MicroMVC подходит более чем «толстый» Symfony.
  • 0
    по моему, использовать для таких целей Silex как-то не то…
    • 0
      Почему вы так считаете? Есть мнение, что Silex немного больше, чем просто микро-фреймворк. Его расширяемость наталкивает на написание таких штук, да и сами разработчики поощряют создание всевозможных дополнений, позиционируя свой продукт как конструктор.

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