Файл .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/
Добавить комментарий