Как превратить Silex в полноценный PHP фреймворк

от автора

SilexДавным давно, в далекой-далекой галактике существовали два основных PHP фреймворка — Symfony и ZF, которые подходили для большинства веб-приложений, срендего — большого масштаба. В отличии от них, следующие поколения этих фреймворков ориентированы на веб-приложения только большого масштаба, сайты же среднего, и, тем более, низкого уровня, на них писать более нерелевантно по отношению к затраченному времени. А после перехода во фриланс, большинство моих заказов можно отнести именно к срендему уровню. На фоне этого, начали появляться микро-фреймворки, один из которых — Silex, от разработчиков Symfony. Изначально он ориентирован на простые сайты, но его легко доработать для разработки сайтов посложнее. Из коробки Silex предоставляет возможность маршрутизации запросов, валидации и фильтрации входящих данных и сервайс контейнер. Этого вполне достаточно для расширения Silex-а во что-то более серьезное. Начнем с разделения на каталоги и файлы. Согласно шаблону MVC — у проекта будут три основных части — это модели, шаблоны (вьюшки) и контроллеры. Помимо этих трех частей, у проекта обычно есть дополнительные библиотеки (в т.ч. и сам Silex), конфиги, статические файлы (картинки, JavaScrip-ы, CSS-стили) и точка входа (Bootstrap). Исходя из этого, изначально можно разделить файлы проекта по таким каталогам:

Файл .htaccess, или его аналог для нужного веб-сервера должен переадресовывать все запросы, за исключением статики (папка web), на index.php, и выглядеть он должен примерно следующим образом:

RewriteEngine On RewriteRule ^web/(.*) web/$1 [L] RewriteRule ^ index.php [L] 

* Соответственно должен быть включен mod_rewrite и AllowOverride равен All.

Изначально файл index.php должен подключать Silex из директории vendor и все контроллеры из директории controller.

// index.php require_once __DIR__.'/vendor/autoload.php';  $app = new Silex\Application();  foreach ( glob(__DIR__."/controller/*.php") as $filename ) {   require_once $filename; }  $app->run(); 

Таким образом, теперь мы можем создать файлы контроллеров, например controller/index.php, в которых объявлять нужные action-ы.

// controller/index.php $app->get('/', function () use ($app) {   return 'Hello Habr'; }); 

Дальше больше, теперь нужно подключить какой-то обработчик шаблонов (вьюшек), Silex предлагает Twig, но для проектов небольшого уровня сложности его я считаю излишним. Для того, чтобы писать вьюшки на чистом PHP, хорошего и красивого костыля для Silex-а не оказалось, пришлось написать самому. К нему есть всего несколько требований, подключение в виде сервиса, вызов метода рендера с передачей параметров, вьюшки, которую нужно сгенерить и лэйаута, в который вьюшка должна бысть встроена. Также нужно иметь возможность вызова другого контроллера из вьюшки, что нужно, например, для рендера каких-то блоков на сайте, данные которого хранятся в БД.

Код

// vendor/Art/View.php namespace Art;  use \Symfony\Component\HttpKernel\HttpKernelInterface; use \Symfony\Component\HttpFoundation\Request;  class View {   private $app = null;   private $blocks = array();    public function __construct($app) {     $this->app = $app;   }    public function render( $layout, $template, $vars = array() ) {     $path = __DIR__ . '/../../view';      foreach ($vars as $key => $value) { $$key = $value; }     $app = $this->app;     ob_start();      require $path . '/' . $template;      $content = ob_get_clean();          if ( null == $layout ) {       return $content;     }          ob_start();     require_once $path . '/' . $layout;     $html = ob_get_clean();      return $html;   }        function renderController($uri) {     $request = $this->app['request'];     $sign = strpos($uri, "?") ? "&" : "?";     $uri = "{$uri}{$sign}subrequest=1";      $subRequest = Request::create(       $uri, 'get', array(), $request->cookies->all(),        array(), $request->server->all()     );          if ( $request->getSession() ) {       $subRequest->setSession( $request->getSession() );     }      $response = $this->app->handle(       $subRequest, HttpKernelInterface::SUB_REQUEST, false     );      if ( !$response->isSuccessful() ) {       throw new \RuntimeException(sprintf(         'Error when rendering "%s" (Status code is %s).',          $request->getUri(), $response->getStatusCode()       ));     }      return $response->getContent();   }      } 

Для его подключения модифицируем index.php

// index.php // ... require_once __DIR__ . '/vendor/Art/View.php'; $app['view'] = $app->share(function () use ($app) {   return new Art\View($app); }); // ... 

Для более простого подключение вендоров, а также для будущего подключения моделей, добавим в бутстрап функцию автолоад.

// index.php // ... spl_autoload_register(function( $className ) {   // Namespace mapping   $namespaces = array(     "Art" => __DIR__ . "/vendor/Art",     "Model" => __DIR__ . "/model"   );    foreach ( $namespaces as $ns => $path ) {     if ( 0 === strpos( $className, "{$ns}\\" ) ) {       $pathArr = explode( "\\", $className );       $pathArr[0] = $path;        $class = implode(DIRECTORY_SEPARATOR, $pathArr);        require_once "{$class}.php";     }   } });  // Services $app['view'] = $app->share(function () use ($app) {   return new Art\View($app); }); // ... 

Теперь создадим лэйаут и вьюшку для главной страницы и модифицируем контроллер для работы с Art\View.

<!-- view/layout.phtml --> <!DOCTYPE html> <html> <head>   <meta charset="utf-8" />   <title>Silex — это круто</title> </head> <body>   <?php echo $this->renderController('/test/') ?>   <?php echo $content ?> </body> </html> 

<!-- view/index/hello.phtml --> Hello <?php echo $name ?> 

// contorller/index.php $app->get('/', function () use ($app) {   $name = "Habr";   return $app['view']->render('layout.phtml', 'index/hello.phtml', array(     'name' => $name   )); });  $app->get('/test/', function () use ($app) {   $test = "Test";   return $app['view']->render(null, 'index/test.phtml', array(     'test' => $test   )); }); 

Получим вывод:

<!DOCTYPE html> <html> <head>   <meta charset="utf-8" />   <title>Silex — это круто</title> </head> <body>   Test  Hello Habr</body> </html> 

Для хранения конфигурационных файлов напишем простой обработчик-парсер ini-файлов.

; conf/app.ini [db] dsn = "mysql://root@localhost/habr;charset=utf8" 
// index.php // ... // Config $app['conf'] = $app->share(function () use ($app) {   $data = parse_ini_file( __DIR__ . '/conf/app.ini', true );   return $data; }); // ... 

Следующим шагом будет подключение ORM, для работы с базой. Silex предлагает Doctrine 2, но, как и с Twig, для проектов небольших Doctrine 2 неоптимальная. Вместо нее я использую минималистичный PHP ActiveRecord.

// index.php // ... // PHPActiveRecord require_once __DIR__ . '/vendor/AR/ActiveRecord.php'; ActiveRecord\Config::initialize(function($cfg) use ($app) {   $cfg->set_model_directory( __DIR__ . '/model');   $cfg->set_connections(array(     'production' => $app['conf']['db']['dsn']   ));    $cfg->set_default_connection('production'); }); // ... 

Создадим базу habr и таблицу test

CREATE DATABASE `habr` DEFAULT CHARSET=utf8;  CREATE TABLE `habr`.`author` (   `id` int(11) NOT NULL AUTO_INCREMENT,   `name` varchar(255) NOT NULL,   PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;  CREATE TABLE `habr`.`book` (   `id` int(11) NOT NULL AUTO_INCREMENT,   `authorId` int(11) NOT NULL,   `name` varchar(255) NOT NULL,   PRIMARY KEY (`id`),   KEY `authorId` (`authorId`),   CONSTRAINT `book_ibfk_1` FOREIGN KEY (`authorId`) REFERENCES `author` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 

Создадим модели

// model/Author.php namespace Model;  class Author extends \ActiveRecord\Model {   static $table_name = 'author';    static $has_many = array(     array('books', 'foreign_key' => 'authorId', 'class_name' => 'Model\Book'),   ); } 

// model/Book.php namespace Model;  class Book extends \ActiveRecord\Model {   static $table_name = 'book';    static $belongs_to = array(     array('author', 'class_name' => 'Model\Author', 'foreign_key' => 'authorId'),   ); } 

Добавим в бутстрап вывод ошибок для локальной версии и зарегистрируем еще один сервис — для генерации ссылок.

// index.php // ... if ( '127.0.0.1' == $_SERVER['REMOTE_ADDR'] ) {   $app['debug'] = true; } // ... // UrlGenerator $app->register(new Silex\Provider\UrlGeneratorServiceProvider()); 

Сделаем выборку

// controller/index.php // ... $app->get('/authors/', function () use ($app) {   $authors = Model\Author::all();    return $app['view']->render('layout.phtml', 'index/authors.phtml', array(     'authors' => $authors   )); });  $app->get('/book/{id}.html', function ($id) use ($app) {   $book = Model\Book::find_by_id($id);   if ( !$book ) {     $app->abort(404, "Book {$id} does not exist.");   }    return $app['view']->render('layout.phtml', 'index/book.phtml', array(     'book' => $book   )); })->bind('book'); 

<!-- view/index/authors.phtml --> <?php foreach ( $authors as $author ): ?> 	<div> 		<?php echo $author->name ?> 		<div> 			<?php foreach ( $author->books as $book ): ?> 				<div> 					<a href="<?php echo $app['url_generator']->generate('book', array('id' => $book->id)) ?>"><?php echo $book->name ?></a> 				</div> 			<?php endforeach ?> 		</div> 	</div> <?php endforeach ?> 

<!-- view/index/book.phtml --> <?php echo $book->name ?> (<?php echo $book->author->name ?>) 

Получим вывод:


Для оформления ошибок в Silex-е есть специальный обработчик.

// index.php $app->error(function (\Exception $e, $code) use ($app) {   // if ( $app['debug'] ) {   //   return;   // }    return $app['view']->render('layout.phtml', 'error.phtml', array(     'msg' => $e->getMessage(),     'code' => $code   )); }); 

<!-- view/error.phtml --> <h1><?php echo $code ?> <?php echo $msg ?></h1> 

Следующая необходимая вещь в любом фреймворке — это формы. Для построения форм Silex предлагает Symfony\Form, но с его зависимостями Silex превращается в Symfony, поэтому используем HTML_QuickForm2.
Качаем в vendor и подключаем:

// index.php // ... // HTML_QuickForm2 set_include_path(   get_include_path() . PATH_SEPARATOR .   __DIR__ . "/vendor/QuickForm2" ); require_once __DIR__ . '/vendor/QuickForm2/HTML/QuickForm2.php'; require_once __DIR__ . '/vendor/QuickForm2/HTML/QuickForm2/Renderer.php'; // ... 

Пропишем контроллер

// controllers/index.php // ... $app->match('/form/', function () use ($app) {   $form = new HTML_QuickForm2('author', 'post', array('action' => ""));   $form->addElement('text', 'name')     ->setlabel('Имя автора')     ->addRule('required', 'Поле обязательно для заполнения');    $form->addElement('button', null, array('type' => 'submit'))     ->setContent('ОК');    if ( $form->isSubmitted() && $form->validate() ) {       $values = $form->getValue();        $author = new Model\Author;       $author->name = $values['name'];          $author->save();        // post POST redirect       return new \Symfony\Component\HttpFoundation\RedirectResponse(         $app['url_generator']->generate('authors')       );   }    return $app['view']->render('layout.phtml', 'index/form.phtml', array(     'form' => $form   )); }); 

И последняя важная вещь — это постраничная навигация. Для нее можно использовать модуль Pagerfanta. Качаем в vendors, подключаем.
Добавляем неймспейс Pagerfanta в автолоадинг:

// index.php // ... // Namespace mapping $namespaces = array(   "Art" => __DIR__ . "/vendor/Art",   "Model" => __DIR__ . "/model",    "Pagerfanta" => __DIR__ . "/vendor/Pagerfanta" ); 

Напишем адаптер для работы с PHP ActiveRecord:

Код

// vendor/Art/PfAdapter.php namespace Art;  class PfAdapter implements \Pagerfanta\Adapter\AdapterInterface {   private $classname = null;   private $params = null;    public function __construct( $classname, $params = array() ) {     $this->classname = $classname;     $this->params = $params;   }    public function getNbResults() {     $params = array(       'select' => 'COUNT(*) as cnt',     );      if ( $this->params ) {     	$params = array_merge($this->params, $params);     }          $cnt = call_user_func_array(       array($this->classname, "all"),        array($params)     );      if ( !$cnt ) {       return 0;     }      return $cnt[0]->cnt;   }    public function getSlice($offset, $length) {     $params = array(       'limit' => $length,       'offset' => $offset     );      if ( $this->params ) {       $params = array_merge($params, $this->params);     }      return call_user_func_array(       array($this->classname, "all"),        array($params)     );   } } 

Добавим паганицию к выборке:

// controller/index.php // ... $app->get('/authors/', function () use ($app) {   $ipp = 3;   $p = $app['request']->get('p', 1);      $adapter = new Art\PfAdapter('Model\Author', array(     'conditions' => 'id < 1000',     'order' => 'id DESC'   ));    $pagerfanta = new Pagerfanta\Pagerfanta($adapter);   $pagerfanta->setMaxPerPage($ipp);   $pagerfanta->setCurrentPage($p);    $view = new Pagerfanta\View\DefaultView;   $html = $view->render($pagerfanta, function($p) use ($app) {     return $app['url_generator']->generate('authors', array('p' => $p));   }, array(     'proximity'         => 3,     'previous_message'  => '« Предыдущая',     'next_message'      => 'Следующая »'   ));    return $app['view']->render('layout.phtml', 'index/authors.phtml', array(     'pagerfanta' => $pagerfanta,     'html' => $html   )); })->bind('authors'); // ... 

<!-- view/index/authors.phtml --> <?php $results = $pagerfanta->getCurrentPageResults() ?>  <?php if ( $results ): ?> 	<?php foreach ( $results as $author ): ?> 		<div> 			<?php echo $author->name ?> 			<div> 				<?php foreach ( $author->books as $book ): ?> 					<div> 						<a href="<?php echo $app['url_generator']->generate('book', array('id' => $book->id)) ?>"><?php echo $book->name ?></a> 					</div> 				<?php endforeach ?> 			</div> 		</div> 	<?php endforeach ?>  	<?php if ( $pagerfanta->haveToPaginate() ): ?> 		<div class="pagerfanta"> 			<?php echo $html ?> 		</div> 	<?php endif ?> <?php else: ?> 	Ничего не найдено <?php endif ?> 

Результат:

В итоге получился легкий и минималистичный фреймворк, пригодный для разработки веб-приложений от маленьких до крупных.
Исходники — habr.zip.
На таком движке написан Open Source аудиоплеер — oplayer.org (https://github.com/uavn/oplayer).

P.S. Есть и Yii, подходящий для задач любого уровня сложности, но часто приходится работать с Symfony2, а Silex на нее больше похож, чем Yii.

ссылка на оригинал статьи http://habrahabr.ru/post/160509/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *