![](https://habrastorage.org/files/641/e2f/00c/641e2f00c3f347999dfe9779cedd63d7.jpg)
Проект на Github
Узнать как установить нужную вам часть руководства, можно в описании к репозиторию по ссылке. (Например, если вы хотите начать с это урока не проходя предыдущий)
Домашняя страница. Записи и комментарии.
Теперь на главной странице выводится список последних записей, но нет никакой информации относительно комментариев к этим записям. У нас есть Сущность Comment благодаря которой мы можем вернуться к главной странице, чтобы предоставить эту информацию. Так как мы установили связь между сущностями Blog и Comment мы знаем, что Doctrine 2 будет иметь возможность получать комментарии к записи (помните, мы добавили объект $comments к сущности Blog). Давайте обновим шаблон главной страницы
{# src/Blogger/BlogBundle/Resources/views/Page/index.html.twig #}
{# .. #}
<footer class="meta">
<p>Comments: {{ blog.comments|length }}</p>
<p>Posted by <span class="highlight">{{ blog.author }}</span> at {{ blog.created|date('h:iA') }}</p>
<p>Tags: <span class="highlight">{{ blog.tags }}</span></p>
</footer>
{# .. #}
Мы использовали comments getter для получения комментариев, а затем передали коллекцию через Twig length фильтр. Если вы посмотрите на домашнюю страницу сейчас, перейдя по адресу http://localhost:8000/ вы увидите число комментариев к каждой записи.
Как объяснялось выше, мы уже сообщили Doctrine 2 что объект $comments сущности Blog будет иметь связь с сущностью Comment. Мы добились этого в предыдущей части с помощью метаданных в сущности Blog.
// src/Blogger/BlogBundle/Entity/Blog.php
/**
* @ORM\OneToMany(targetEntity="Comment", mappedBy="blog")
*/
protected $comments;
Итак, мы знаем, что Doctrine 2 известно о взаимосвязи между записями и комментариями, но как это заполняет объект $comments соответствующими сущностями Comment? Если вы помните метод в BlogRepository, который мы создали (показан ниже) получающий на домашней странице записи, мы не делали выборку для извлечения связанных Comment сущностей.
// src/Blogger/BlogBundle/Repository/BlogRepository.php
public function getLatestBlogs($limit = null)
{
$qb = $this->createQueryBuilder('b')
->select('b')
->addOrderBy('b.created', 'DESC');
if (false === is_null($limit))
$qb->setMaxResults($limit);
return $qb->getQuery()
->getResult();
}
Однако, Doctrine 2 использует процесс, называемый отложенной загрузкой, где сущности Comment извлекаются из базы данных в случае необходимости, в нашем случае, когда вызывается {{blog.comments | length}}. Мы можем продемонстрировать этот процесс, с помощью панели инструментов разработчика. Мы уже начали изучать основы панели инструментов разработчика и настало время, чтобы представить одну из самых полезных её функций, Doctrine 2 профайлера. Doctrine 2 профайлер можно открыть, щелкнув значок на панели инструментов разработчика… Число рядом с этой пиктограммой показывает количество запросов, выполненных к базе данных для текущего HTTP-запроса.
![](https://habrastorage.org/files/e24/4ff/0fd/e244ff0fd2394a83af2c967ecf4d0075.jpg)
Если щелкнуть значок Doctrine 2 вам будет представлена информация о запросах, которые были выполнены Doctrine 2 к базе данных для текущего запроса HTTP.
![](https://habrastorage.org/files/e0d/a37/21a/e0da3721a90a4ff184e77d2ab4077919.png)
Как вы можете видеть в приведенном выше скриншоте, там есть целый ряд выполняемых запросов для вывода на главную страницу. Второй запрос, извлекает сущности записей из базы данных и выполняется в результате метода getLatestBlogs() класса BlogRepository. После этого запроса вы заметите ряд запросов, которые получают комментарии из базы данных, одну запись за один раз. Мы можем увидеть это здесь WHERE t0.blog_id = ? в каждом из запросов, где ? заменяется на значение параметра (id блога). Каждый из этих запросов является результатом вызова {{blog.comments}} в шаблоне домашней страницы. Каждый раз, когда эта функция выполняется, Doctrine 2 должен лениво(lazily) загрузить сущности Comment, которые относятся к сущности Blog.
В то время как отложенная загрузка очень эффективна при извлечении связанных сущностей из базы данных, это не всегда является наиболее эффективным способом для выполнения этой задачи. Doctrine2 предоставляет возможность объединить связанные объекты вместе, когда выполняются запросы к базе данных. Таким образом, мы можем вернуть сущность Blog и связанные с ней сущности Comment из базы данных в одном запросе. Обновите код QueryBuilder в BlogRepository.
// src/Blogger/BlogBundle/Repository/BlogRepository.php
public function getLatestBlogs($limit = null)
{
$qb = $this->createQueryBuilder('b')
->select('b, c')
->leftJoin('b.comments', 'c')
->addOrderBy('b.created', 'DESC');
if (false === is_null($limit))
$qb->setMaxResults($limit);
return $qb->getQuery()
->getResult();
}
Теперь, если вы обновите главную страницу и посмотрите на вывод Doctrine 2 в панели инструментов разработчика вы заметите, что количество запросов сократилось. Вы также увидите, что таблица comment была присоединена к таблице blog.
Ленивая загрузка и объединение связанных объектов являются очень мощными концепциями, но они должны быть использованы правильно. Правильный баланс между 2 концепциями должен быть найден для того, чтобы обеспечить вашему приложению эффективность, настолько насколько это возможно. На первый взгляд может показаться хорошей идеей объединять каждые связанные сущности, так что вам никогда не придется пользоваться ленивой нагрузкой и количество запросов к базе данных всегда будет оставаться низким. Однако важно помнить, что чем больше информации вы извлекаете из базы данных, тем больше придётся обрабатывать Doctrine 2. Больше данных означает также больший объем памяти, используемый сервером для хранения объектов сущностей.
Прежде чем двигаться дальше давайте сделаем одно небольшое дополнение к шаблону домашней страницы, для количества комментариев, которые мы только что добавили. Обновите шаблон домашней страницы
{# src/Blogger/BlogBundle/Resources/views/Page/index.html.twig #}
{# .. #}
<footer class="meta">
<p>Comments: <a href="{{ path('BloggerBlogBundle_blog_show', { 'id': blog.id }) }}#comments">{{ blog.comments|length }}</a></p>
<p>Posted by <span class="highlight">{{ blog.author }}</span> at {{ blog.created|date('h:iA') }}</p>
<p>Tags: <span class="highlight">{{ blog.tags }}</span></p>
</footer>
{# .. #}
чтобы при нажатии на количество комментариев мы переходили к комментариям соответствующей записи.
Боковая панель (sidebar)
В настоящее время боковая панель symblog выглядит пустой. Мы обновим её добавив распространённые элементы присущие большинству блогов Облако тегов и Последние комментарии.
Облако тегов
Облако тегов показывает теги для каждой записи обращая внимание на более популярные теги, путем выделения их более жирным шрифтом. Для достижения этой цели нам нужен способ для получения всех тегов для всех записей. Давайте создадим несколько новых методов в классе BlogRepository для этого. Обновите класс BlogRepository
// src/Blogger/BlogBundle/Repository/BlogRepository.php
public function getTags()
{
$blogTags = $this->createQueryBuilder('b')
->select('b.tags')
->getQuery()
->getResult();
$tags = array();
foreach ($blogTags as $blogTag)
{
$tags = array_merge(explode(",", $blogTag['tags']), $tags);
}
foreach ($tags as &$tag)
{
$tag = trim($tag);
}
return $tags;
}
public function getTagWeights($tags)
{
$tagWeights = array();
if (empty($tags))
return $tagWeights;
foreach ($tags as $tag)
{
$tagWeights[$tag] = (isset($tagWeights[$tag])) ? $tagWeights[$tag] + 1 : 1;
}
// Shuffle the tags
uksort($tagWeights, function() {
return rand() > rand();
});
$max = max($tagWeights);
// Max of 5 weights
$multiplier = ($max > 5) ? 5 / $max : 1;
foreach ($tagWeights as &$tag)
{
$tag = ceil($tag * $multiplier);
}
return $tagWeights;
}
Так как теги хранятся в базе данных в виде значений, разделенных запятыми (CSV) нам нужен способ, чтобы разделить их и вернуть в виде массива. Это достигается с помощью метода getTags(). Метод getTagWeights() может использовать массив тегов, чтобы вычислить «вес» каждого тега на основе его популярности, в массиве. Тэги перемешиваются, чтобы отображаться в случайном порядке на странице.
У нас есть возможность сгенерировать облако тегов, теперь нам надо его отобразить. Создайте новый метод в контроллере
// src/Blogger/BlogBundle/Controller/PageController.php
public function sidebarAction()
{
$em = $this->getDoctrine()
->getManager();
$tags = $em->getRepository('BloggerBlogBundle:Blog')
->getTags();
$tagWeights = $em->getRepository('BloggerBlogBundle:Blog')
->getTagWeights($tags);
return $this->render('BloggerBlogBundle:Page:sidebar.html.twig', array(
'tags' => $tagWeights
));
}
Метод очень прост, он использует 2 новых метода BlogRepository для создания облака тегов, и передает это в отображение. Теперь давайте создадим шаблон
{# src/Blogger/BlogBundle/Resources/views/Page/sidebar.html.twig #}
<section class="section">
<header>
<h3>Tag Cloud</h3>
</header>
<p class="tags">
{% for tag, weight in tags %}
<span class="weight-{{ weight }}">{{ tag }}</span>
{% else %}
<p>There are no tags</p>
{% endfor %}
</p>
</section>
Шаблон также очень прост. Он просто перебирает различные теги и устанавливает класс в соответствии с «весом» тега. Цикл for показывает, как получить доступ к ключам и значениям пар массива, где tag является ключом, а weight значением. Существует несколько вариантов использования цикла for, приведенных в документации Twig.
Если вы посмотрите на основной шаблон макета BloggerBlogBundle, расположенного src/Blogger/BlogBundle/Resources/views/layout.html.twig вы заметите, что мы помещаем заполнитель(placeholder) для блока боковой панели. Давайте теперь заменим это выводом нового метода для боковой панели. Вспомните из предыдущей части, что метод Twig render будет выводить содержимое метода контроллера, в этом случае метода sidebar контроллера Page.
{# src/Blogger/BlogBundle/Resources/views/layout.html.twig #}
{# .. #}
{% block sidebar %}
{{ render(controller('BloggerBlogBundle:Page:sidebar' ))}}
{% endblock %}
Наконец давайте добавим стили для Облака тегов. Добавьте новые стили в файл
.sidebar .section { margin-bottom: 20px; }
.sidebar h3 { line-height: 1.2em; font-size: 20px; margin-bottom: 10px; font-weight: normal; background: #eee; padding: 5px; }
.sidebar p { line-height: 1.5em; margin-bottom: 20px; }
.sidebar ul { list-style: none }
.sidebar ul li { line-height: 1.5em }
.sidebar .small { font-size: 12px; }
.sidebar .comment p { margin-bottom: 5px; }
.sidebar .comment { margin-bottom: 10px; padding-bottom: 10px; }
.sidebar .tags { font-weight: bold; }
.sidebar .tags span { color: #000; font-size: 12px; }
.sidebar .tags .weight-1 { font-size: 12px; }
.sidebar .tags .weight-2 { font-size: 15px; }
.sidebar .tags .weight-3 { font-size: 18px; }
.sidebar .tags .weight-4 { font-size: 21px; }
.sidebar .tags .weight-5 { font-size: 24px; }
Так как мы добавили новые стили, давайте подключим их. Обновите BloggerBlogBundle main шаблон
{# src/Blogger/BlogBundle/Resources/views/layout.html.twig #}
{# .. #}
{% block stylesheets %}
{{ parent() }}
<link href="{{ asset('bundles/bloggerblog/css/blog.css') }}" type="text/css" rel="stylesheet" />
<link href="{{ asset('bundles/bloggerblog/css/sidebar.css') }}" type="text/css" rel="stylesheet" />
{% endblock %}
{# .. #}
Заметка
Если вы не используете метод символических ссылок для обращения к assets бандла в папке web, вы должны повторно запустить команду установки assets.
$ php app/console assets:install web
Если теперь вы обновите главную страницу блога вы увидите Облако тегов в боковой панели. Для того, чтобы получить теги с различными «весами» (популярностью), вам может понадобиться обновить фикстуры блога.
Последние Комментарии
Теперь у нас есть Облако тегов, давайте добавим компонент Последние комментарии на боковую панель.
Во-первых, нам нужен способ для получения последних комментариев к записям. Для этого мы добавим новый метод в CommentRepository
<?php
// src/Blogger/BlogBundle/Repository/CommentRepository.php
public function getLatestComments($limit = 10)
{
$qb = $this->createQueryBuilder('c')
->select('c')
->addOrderBy('c.id', 'DESC');
if (false === is_null($limit))
$qb->setMaxResults($limit);
return $qb->getQuery()
->getResult();
}
Далее обновим метод sidebar для получения последних комментариев и передачи их в шаблон
// src/Blogger/BlogBundle/Controller/PageController.php
public function sidebarAction()
{
// ..
$commentLimit = $this->container
->getParameter('blogger_blog.comments.latest_comment_limit');
$latestComments = $em->getRepository('BloggerBlogBundle:Comment')
->getLatestComments($commentLimit);
return $this->render('BloggerBlogBundle:Page:sidebar.html.twig', array(
'latestComments' => $latestComments,
'tags' => $tagWeights
));
}
Вы заметите что мы использовали новый параметр blogger_blog.comments.latest_comment_limit для лимита количества полученных комментариев. Чтобы создать этот параметр, обновите файл конфигурации
# src/Blogger/BlogBundle/Resources/config/config.yml
parameters:
# ..
# Blogger max latest comments
blogger_blog.comments.latest_comment_limit: 10
Наконец, мы должны отобразить последние комментарии в шаблоне боковой панели. Обновите шаблон
{# src/Blogger/BlogBundle/Resources/views/Page/sidebar.html.twig #}
{# .. #}
<section class="section">
<header>
<h3>Latest Comments</h3>
</header>
{% for comment in latestComments %}
<article class="comment">
<header>
<p class="small"><span class="highlight">{{ comment.user }}</span> commented on
<a href="{{ path('BloggerBlogBundle_blog_show', { 'id': comment.blog.id }) }}#comment-{{ comment.id }}">
{{ comment.blog.title }}
</a>
[<em><time datetime="{{ comment.created|date('c') }}">{{ comment.created|date('Y-m-d h:iA') }}</time></em>]
</p>
</header>
<p>{{ comment.comment }}</p>
</p>
</article>
{% else %}
<p>There are no recent comments</p>
{% endfor %}
</section>
Если теперь вы обновите страницу вы увидите Последние комментарии, которые выводятся под блоком Облако тегов.
![](https://habrastorage.org/files/fe1/7c0/031/fe17c00312ee427b8beacb45815b637f.png)
Расширения Twig
До сих пор мы, отображали дату добавления комментария блога в стандартном формате даты, 2011-04-21. Гораздо лучший подход будет заключаться в том, чтобы отображать не дату, а время, которое прошло с момента публикации комментария, например, Опубликовано 3 часа назад. Мы могли бы добавить метод в сущность Comment для достижения этой цели и изменить шаблоны, чтобы использовать этот метод вместо метода {{comment.created|date (' h:iA Y-m-d')}}.
Но так как нам может понадобиться использовать эту функциональность в другом месте было бы лучше, вынести метод из сущности Comment. Так как преобразование даты задача именно шаблона, мы должны осуществить это используя Twig. Twig дает нам эту возможность, предоставляя интерфейс Extension.
Мы можем использовать интерфейс extension в Twig для расширения функциональных возможностей, которые он предоставляет по умолчанию. Мы собираемся создать новое Twig расширение фильтр, который можно будет использовать следующим образом.
{{ comment.created|created_ago }}. Это вернет дату создания комментария в формате, Опубликовано 2 дня назад.
Расширение
Создайте файл для расширения Twig со следующим содержимым
<?php
// src/Blogger/BlogBundle/Twig/Extensions/BloggerBlogExtension.php
namespace Blogger\BlogBundle\Twig\Extensions;
class BloggerBlogExtension extends \Twig_Extension
{
public function getFilters()
{
return array(
new \Twig_SimpleFilter('created_ago', array($this, 'createdAgo')),
);
}
public function createdAgo(\DateTime $dateTime)
{
$delta = time() - $dateTime->getTimestamp();
if ($delta < 0)
throw new \InvalidArgumentException("createdAgo is unable to handle dates in the future");
$duration = "";
if ($delta < 60)
{
// Seconds
$time = $delta;
$duration = $time . " second" . (($time > 1) ? "s" : "") . " ago";
}
else if ($delta <= 3600)
{
// Mins
$time = floor($delta / 60);
$duration = $time . " minute" . (($time > 1) ? "s" : "") . " ago";
}
else if ($delta <= 86400)
{
// Hours
$time = floor($delta / 3600);
$duration = $time . " hour" . (($time > 1) ? "s" : "") . " ago";
}
else
{
// Days
$time = floor($delta / 86400);
$duration = $time . " day" . (($time > 1) ? "s" : "") . " ago";
}
return $duration;
}
public function getName()
{
return 'blogger_blog_extension';
}
}
Создание расширения является простой задачей. Мы перезапишем метод getFilters() и вернем любое количество фильтров, которое мы хотим. В данном случае мы создаем фильтр created_ago. Этот фильтр затем зарегистрирует использование метода createdAgo, который просто преобразует объект DateTime в строку, представляющую продолжительность времени прошедшее с момента сохранения значения в объект DateTime.
Регистрация Расширения
Для того, чтобы расширение Twig стало нам доступно, необходимо обновить файл служб
services:
blogger_blog.twig.extension:
class: Blogger\BlogBundle\Twig\Extensions\BloggerBlogExtension
tags:
- { name: twig.extension }
Как видите это регистрирует новый сервис используя класс расширения BloggerBlogExtension Twig который мы только что создали.
Отображение
Теперь новый Twig фильтр готов к использованию. Давайте обновим список Последних комментариев в боковой панели используя created_at фильтр. Обновите шаблон боковой панели
{# src/Blogger/BlogBundle/Resources/views/Page/sidebar.html.twig #}
{# .. #}
<section class="section">
<header>
<h3>Latest Comments</h3>
</header>
{% for comment in latestComments %}
{# .. #}
<em><time datetime="{{ comment.created|date('c') }}">{{ comment.created|created_ago }}</time></em>
{# .. #}
{% endfor %}
</section>
Если теперь вы введете в ваш браузер http://localhost:8000/ вы увидите даты последних комментариев, с использованием фильтра Twig.
Давайте также обновим комментарии на странице блога с использованием нового фильтра. Замените контент в шаблоне
{# src/Blogger/BlogBundle/Resources/views/Comment/index.html.twig #}
{% for comment in comments %}
<article class="comment {{ cycle(['odd', 'even'], loop.index0) }}" id="comment-{{ comment.id }}">
<header>
<p><span class="highlight">{{ comment.user }}</span> commented <time datetime="{{ comment.created|date('c') }}">{{ comment.created|created_ago }}</time></p>
</header>
<p>{{ comment.comment }}</p>
</article>
{% else %}
<p>There are no comments for this post. Be the first to comment...</p>
{% endfor %}
Совет
Есть целый ряд полезных расширений Twig, доступных через библиотеку Twig-Extensions на GitHub. Если вы создали полезное расширение вы можете создать pull request для этого репозитория, и расширение может быть добавлено, чтобы другие люди могли его использовать.
Url
В настоящее время URL для каждой записи в блоге отображает только id записи. Хотя это вполне приемлемо с функциональной точки зрения, это не является хорошим решением для SEO. Например, URL http://localhost:8000/1 не дает никакой информации о содержании записи, что-то вроде http://localhost:8000/1/a-day-with-symfony2 было бы гораздо лучше. Для достижения этой цели мы добавим slug в название блога и используем его как часть этого URL. Добавление slug удалит все символы, не являющиеся ASCII и заменит их на дефис — .
Обновите маршрут
Давайте модифицируем правило маршрута для страницы записи и добавим компонент slug. Обновите правило маршрута
# src/Blogger/BlogBundle/Resources/config/routing.yml
BloggerBlogBundle_blog_show:
path: /{id}/{slug}
defaults: { _controller: "BloggerBlogBundle:Blog:show" }
requirements:
methods: GET
id: \d+
Контроллер
Как и в случае существующего компонента id, новый компонент slug будет передан в действие контроллера в качестве аргумента, так что давайте обновим контроллер
// src/Blogger/BlogBundle/Controller/BlogController.php
public function showAction($id, $slug)
{
// ..
}
чтобы это отразить.
Заметка
Порядок, в котором аргументы передаются в действие контроллера не имеет значения, имеют значение только имена. Symfony2 способен сопоставлять аргументы маршрутизации со списком параметров. Так как мы еще не использовали значения компонентов по умолчанию, стоит упомянуть их здесь. Если мы добавляем еще один компонент для правила маршрутизации мы можем указать значение по умолчанию с помощью опции defaults.
BloggerBlogBundle_blog_show: path: /{id}/{slug} defaults: { _controller: "BloggerBlogBundle:Blog:show", comments: true } requirements: methods: GET id: \d+
public function showAction($id, $slug, $comments) { // .. }
Используя этот метод, запрос к http://localhost:8000/1/symfony2-blog приведет к тому что в $comments будет установлено значение true в методе showAction
Slug
Поскольку мы хотим, создавать slug из названия записи, мы будем автоматически генерировать значение slug. Мы могли бы просто выполнить это действие во время вывода поля заголовка, но вместо этого мы будем хранить slug в сущности Blog и сохранять в базу данных.
Обновление сущности Blog
Давайте добавим новое свойство в сущности Blog для хранения slug. Обновите сущность
// src/Blogger/BlogBundle/Entity/Blog.php
class Blog
{
// ..
/**
* @ORM\Column(type="string")
*/
protected $slug;
// ..
}
Теперь создайте методы доступа для нового свойства $slug. Как и раньше выполним команду.
$ php app/console doctrine:generate:entities Blogger
Далее обновим схему базы данных.
$ php app/console doctrine:migrations:diff
$ php app/console doctrine:migrations:migrate
Для генерации значения slug мы будем использовать метод slugify из symfony1 Jobeet руководства. Добавьте метод slugify для сущности Blog
// src/Blogger/BlogBundle/Entity/Blog.php
public function slugify($text)
{
// replace non letter or digits by -
$text = preg_replace('#[^\\pL\d]+#u', '-', $text);
// trim
$text = trim($text, '-');
// transliterate
if (function_exists('iconv'))
{
$text = iconv('utf-8', 'us-ascii//TRANSLIT', $text);
}
// lowercase
$text = strtolower($text);
// remove unwanted characters
$text = preg_replace('#[^-\w]+#', '', $text);
if (empty($text))
{
return 'n-a';
}
return $text;
}
Поскольку мы хотим, чтобы slug генерировался автоматически из заголовка мы можем генерировать slug, когда устанавливается значение заголовка. Для этого мы можем обновить метод setTitle чтобы он устанавливал значение slug. Обновите сущность Blog
// src/Blogger/BlogBundle/Entity/Blog.php
public function setTitle($title)
{
$this->title = $title;
$this->setSlug($this->title);
}
Далее обновите метод setSlug.
// src/Blogger/BlogBundle/Entity/Blog.php
public function setSlug($slug)
{
$this->slug = $this->slugify($slug);
}
Далее перезагрузим данные фикстур чтобы сгенерировать slug.
$ php app/console doctrine:fixtures:load
Обновление маршрутов
Наконец, нам нужно обновить существующие вызовы для создания маршрутов к странице блога. Есть несколько мест где нам нужно это сделать.
Откройте шаблон домашней страницы и добавьте следующее
{# src/Blogger/BlogBundle/Resources/views/Page/index.html.twig #}
{% extends 'BloggerBlogBundle::layout.html.twig' %}
{% block body %}
{% for blog in blogs %}
<article class="blog">
<div class="date"><time datetime="{{ blog.created|date('c') }}">{{ blog.created|date('l, F j, Y') }}</time></div>
<header>
<h2><a href="{{ path('BloggerBlogBundle_blog_show', { 'id': blog.id, 'slug': blog.slug }) }}">{{ blog.title }}</a></h2>
</header>
<img src="{{ asset(['images/', blog.image]|join) }}" />
<div class="snippet">
<p>{{ blog.blog(500) }}</p>
<p class="continue"><a href="{{ path('BloggerBlogBundle_blog_show', { 'id': blog.id, 'slug': blog.slug }) }}">Continue reading...</a></p>
</div>
<footer class="meta">
<p>Comments: <a href="{{ path('BloggerBlogBundle_blog_show', { 'id': blog.id, 'slug': blog.slug }) }}#comments">{{ blog.comments|length }}</a></p>
<p>Posted by <span class="highlight">{{ blog.author }}</span> at {{ blog.created|date('h:iA') }}</p>
<p>Tags: <span class="highlight">{{ blog.tags }}</span></p>
</footer>
</article>
{% else %}
<p>There are no blog entries for symblog</p>
{% endfor %}
{% endblock %}
Кроме того, одно обновление должно быть сделано в блоке Последние комментарии шаблона боковой панели
{# src/Blogger/BlogBundle/Resources/views/Page/sidebar.html.twig #}
{# .. #}
<a href="{{ path('BloggerBlogBundle_blog_show', { 'id': comment.blog.id, 'slug': comment.blog.slug }) }}#comment-{{ comment.id }}">
{{ comment.blog.title }}
</a>
{# .. #}
Наконец должен быть обновлен метод createAction из Comment контроллера при перенаправлении на страницу блога при успешной публикации комментария. Обновите контроллер Comment
// src/Blogger/BlogBundle/Controller/CommentController.php
public function createAction($blog_id)
{
// ..
if ($form->isValid()) {
// ..
return $this->redirect($this->generateUrl('BloggerBlogBundle_blog_show', array(
'id' => $comment->getBlog()->getId(),
'slug' => $comment->getBlog()->getSlug())) .
'#comment-' . $comment->getId()
);
}
// ..
}
Теперь, если вы перейдете на домашнюю страницу http://localhost:8000/ и нажмёте на один из заголовков записей вы увидите, что slug записи был добавлен к концу URL.
Окружения
Окружения являются очень мощными и в то же время простыми функциями Symfony2. С помощью окружений мы можем настроить различные аспекты Symfony2 для запуска в зависимости от конкретных потребностей по-разному в течение всего жизненного цикла приложения. По умолчанию Symfony2 настроен работать с 3-мя Окружениями:
dev — Разработка
test — Тестирование
prod — Рабочее
Цель этих окружений не требует пояснений, но что, если эти окружения должны быть настроены по-разному в зависимости от индивидуальных потребностей. При разработке приложения полезно иметь панель инструментов для разработчика с отображаемыми ошибками, в то время как в рабочем окружении вы не хотите этого отображать. На самом деле, если это информация будет отображаться она будет представлять угрозу безопасности так как будет доступно много деталей относительно внутренности приложения и сервера. В рабочем окружении было бы лучше отображать пользовательские страницы ошибок с помощью упрощенных сообщений, в то время как информация будет записываться в текстовые файлы. Было бы также полезно включить кэширование, чтобы обеспечить приложению хорошую работу. Если кэширование будет включено в окружении разработки — это будет доставлять массу неудобств так как вам придётся очищать кэш каждый раз, когда вы изменяете файлы конфигураций и т.д.
Окружение test является тестовой средой. Оно используется при выполнении тестов, таких как unit или функциональное тестирование. Мы ещё не рассматривали тестирование, оно будет рассмотрено в следующей части…
Front контроллер
До этого момента мы работали в окружении разработчика. Если мы взглянем на фронт-контроллер расположенный в web/app_dev.php вы увидите следующую строку:
$kernel = new AppKernel('dev', true);
В противоположность этому, если мы посмотрим на фронт-контроллер для рабочего окружения, расположенного в web/app.php мы увидим следующее:
$kernel = new AppKernel('prod', false);
Вы можете видеть, что в данном случае в AppKernel передаётся рабочее окружение.
Тестовое окружение не должно быть запущено в браузере и поэтому отсутствует фронт-контроллер app_test.php.
Параметры конфигурации
Выше мы видели, как фронт-контроллеры используются для изменения окружения в котором, работает приложение. Теперь мы рассмотрим, как различные настройки изменяются во время выполнения в каждом окружении. Если вы посмотрите на файлы в app/config вы увидите несколько файлов config.yml. В частности, есть один основной config.yml и 3 других все суффиксом окружения; config_dev.yml, config_test.yml и config_prod.yml. Каждый из этих файлов загружается в зависимости от текущего окружения. Если мы исследуем файл config_dev.yml вы увидите следующие строки в верхней части.
imports:
- { resource: config.yml }
Директива imports заставит включить config.yml, в этот файл Та же директива imports может быть найдена в верхней части 2 других конфигурационных файлов окружений, config_test.yml и config_prod.yml. Включив общий набор параметров конфигурации, определенных в config.yml мы имеем возможность переопределять конкретные параметры для каждой среды. Мы можем увидеть в файле конфигурации development, расположенного app/config/config_dev.yml следующие строки, конфигурации использования панели инструментов разработчика.
# app/config/config_dev.yml
web_profiler:
toolbar: true
Эта настройка отсутствует в конфигурационном файле рабочего окружения так как мы не хотим, чтобы отображалась панель инструментов разработчика.
Запуск рабочего окружения
Для тех из вас кто хотел увидеть, как сайт работает в рабочем окружении сейчас самое время.
Во-первых, мы должны очистить кэш с помощью одной из команд Symfony2.
$ php app/console cache:clear --env=prod
Теперь введите в ваш браузер http://localhost:8000/app.php.
Вы заметите, что сайт выглядит так же, но есть несколько важных различий. Панель инструментов разработчика и подробное сообщение об ошибке больше не отображаются, попытайтесь перейти http://localhost:8000/app.php/999.
![](https://habrastorage.org/files/50b/e85/25c/50be8525cbbd44cdb6e5336d4f3db1e9.png)
Подробное сообщение об ошибке было заменено упрощенным сообщением, информирующим пользователя о проблеме. Эти страницы могут быть настроены в соответствие с внешним видом вашего приложения.
Кроме того, вы заметите что файл app/logs/prod.log заполняется данными относительно выполнения. Это бывает полезным, когда у вас есть проблемы с приложением в рабочем окружении, так как ошибки не могут быть больше отображены на экране.
Заметка
Как запрос на http://localhost:8000/app.php в конечном итоге направляется через файл app.php? Я уверен, все вы создавали файлы, такие как index.html и index.php, которые действуют как индекс сайта, но как app.php может стать таким? Это стало возможным благодаря RewriteRule в файле web/.htaccess
RewriteRule ^(.*)$ app.php [QSA,L]
Мы можем видеть, что эта строка имеет регулярное выражение, которое соответствует любому тексту, ^(.*)$ и передано в app.php.
Если работаете на сервере Apache, у которого не включен mod_rewrite.c, вы можете просто добавить app.php к URL, http://localhost:8000/app.php/.
Создание нового окружения
Создание собственного окружения в Symfony2 может понадобится, например, когда вы захотите создать промежуточное окружение, которое будет работать на рабочем сервере, но будет выводить некоторую информацию для отладки. Это позволило бы тестировать платформу вручную на фактическом рабочем сервере, так как конфигурации рабочего сервера и сервера разработчика могут отличаться.
Так как создание нового окружения является простой задачей, она выходит за рамки этого руководства. Существует отличная статья в cookbook Symfony2, в которой это описано.
Assetic
Так как начиная с версии Symfony2.8 библиотека Assetiс не поставляется по умолчанию, мы должны сами установить бандл assetic. Добавьте строчку в файл composer.json в корне проекта
"require": {
//...
"symfony/assetic-bundle": "dev-master"
},
и введите команду
composer update
в консоли.
Далее зарегистрируем бандл в файле
//app/AppKernel.php
public function registerBundles()
{
$bundles = array(
// ...
new Symfony\Bundle\AsseticBundle\AsseticBundle(),
);
// ...
}
И в конце добавим минимально необходимые настройки в файл
# app/config/config.yml
assetic:
debug: '%kernel.debug%'
use_controller: '%kernel.debug%'
filters:
cssrewrite: ~
# ...
Библиотека была разработана Kris Wallsmith под вдохновением Python библиотеки webassets.
Assetic имеет дело с 2-мя частями управления assets, assets такие как изображения, таблицы стилей, JavaScript и фильтры, которые могут быть применены к ним. Эти фильтры могут выполнять полезные задачи, такие как минификация ваших CSS и JavaScript, передача файлов CoffeeScript через компилятор CoffeeScript и объединения assets файлов вместе, чтобы уменьшить количество запросов HTTP сделанных на сервере.
В настоящее время мы используем Twig asset функцию для включения assets в шаблон следующим образом.
<link href="{{ asset('bundles/bloggerblog/css/blog.css') }}" type="text/css" rel="stylesheet" />
Assets
Assetic библиотека описывает asset следующим образом:
Assetic asset это что-то с фильтруемым контентом, который может быть загружен. Asset также включает в себя метаданные, некоторыми из которых можно манипулировать.
Проще говоря, assets являются ресурсами которые использует ваше приложение, например, таблицы стилей и изображения.
Чтобы включить Assetic для BloggerBlogBundle мы должны изменить
# ..
assetic:
bundles: [BloggerBlogBundle]
# ..
Это позволит включить Assetic только для BloggerBlogBundle и потребует корректировки всякий раз, когда новый бандл должен использовать Assetic. Мы можем полностью удалить строку bundles и включить его для всех будущих бандлов.
Таблицы стилей
Давайте начнем с замены текущих вызовов asset для таблиц стилей в основном шаблоне BloggerBlogBundle. Обновите контент в шаблоне
{# src/Blogger/BlogBundle/Resources/views/layout.html.twig #}
{# .. #}
{% block stylesheets %}
{{ parent () }}
{% stylesheets
'@BloggerBlogBundle/Resources/public/css/*'
%}
<link href="{{ asset_url }}" rel="stylesheet" media="screen" />
{% endstylesheets %}
{% endblock %}
{# .. #}
Мы заменили предыдущие 2 ссылки на CSS-файлы, некоторой Assetic функциональностью. Использованием таблиц стилей из Assetic мы определили, что все CSS файлы в src/Blogger/BlogBundle/Resources/public/css должны быть объединены в 1 файл, а затем выведены. Объединение файлов является очень простым, но эффективным способом оптимизации вашего сайта за счет уменьшения количества необходимых файлов. Меньшее количество файлов означает меньшее количество HTTP-запросов к серверу. В то время как мы использовали * чтобы указать все файлы в каталоге css мы могли бы просто перечислить каждый файл по отдельности.
{# src/Blogger/BlogBundle/Resources/views/layout.html.twig #}
{# .. #}
{% block stylesheets %}
{{ parent () }}
{% stylesheets
'@BloggerBlogBundle/Resources/public/css/blog.css'
'@BloggerBlogBundle/Resources/public/css/sidebar.css'
%}
<link href="{{ asset_url }}" rel="stylesheet" media="screen" />
{% endstylesheets %}
{% endblock %}
{# .. #}
Конечный результат в обоих случаях одинаков. Первый вариант с использованием * гарантирует, что, когда новые CSS файлы добавятся в каталог, они всегда будут включены в объединенный файл CSS Assetic. Это может быть не тем что вам требуется, так что используйте любой метод описанный выше для удовлетворения ваших потребностей.
Если вы посмотрите на код HTML http://localhost:8000/ вы увидите, что CSS файлы были включены примерно таким образом (Заметьте, мы находимся в окружении разработчика).
<link href="/css/d8f44a4_part_1_blog_1.css" rel="stylesheet" media="screen" />
<link href="/css/d8f44a4_part_1_sidebar_2.css" rel="stylesheet" media="screen" />
Вы можете быть удивлены, почему здесь 2 файла. Ведь выше было указано, что Assetic должен объединять файлы в 1 CSS файл. Это происходит потому, что мы запустили symblog в окружении разработчика. Мы можем попросить Assetic работать в режиме без отладки, установив флаг отладки в false
{# src/Blogger/BlogBundle/Resources/views/layout.html.twig #}
{# .. #}
{% stylesheets
'@BloggerBlogBundle/Resources/public/css/*'
debug=false
%}
<link href="{{ asset_url }}" rel="stylesheet" media="screen" />
{% endstylesheets %}
{# .. #}
Теперь, если вы посмотрите на HTML код вы увидите что-то вроде этого.
<link href="/css/3c7da45.css" rel="stylesheet" media="screen" />
Если просмотреть содержимое этого файла вы увидите, что 2 CSS файла, blog.css и sidebar.css были объединены в 1 файл. Имя созданного файла CSS генерируется случайным образом. Если вы хотите контролировать имя, данное сгенерированному файлу используйте output опцию
{% stylesheets
'@BloggerBlogBundle/Resources/public/css/*'
output='css/blogger.css'
%}
<link href="{{ asset_url }}" rel="stylesheet" media="screen" />
{% endstylesheets %}
Перед тем, как продолжить удалите флаг отладки из предыдущего фрагмента, так как мы хотим, возобновить поведение по умолчанию для assets.
Нам также необходимо обновить базовый шаблон приложения
{# app/Resources/views/base.html.twig #}
{# .. #}
{% block stylesheets %}
<link href='http://fonts.googleapis.com/css?family=Irish+Grover' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=La+Belle+Aurore' rel='stylesheet' type='text/css'>
{% stylesheets
'css/*'
%}
<link href="{{ asset_url }}" rel="stylesheet" media="screen" />
{% endstylesheets %}
{% endblock %}
{# .. #}
JavaScripts
Так как в настоящее время нет каких-либо файлов JavaScript в нашем приложении, их использование в Assetic является таким же, как и для
{% javascripts
'@BloggerBlogBundle/Resources/public/js/*'
%}
<script type="text/javascript" src="{{ asset_url }}"></script>
{% endjavascripts %}
Фильтры
Реальная сила Assetic — это фильтры. Фильтры могут быть применены к assets или коллекции assets. Есть большое количество фильтров, расположенных внутри ядра библиотеки, включая следующие:
CssMinFilter: минифицирует CSS
JpegoptimFilter: оптимизирует изображения формата JPEG
Yui\CssCompressorFilter: сжимает CSS файлы используя YUI компрессор
Yui\JsCompressorFilter: сжимает JavaScript файлы используя YUI компрессор
CoffeeScriptFilter: компилирует CoffeeScript в JavaScript
Полный список доступных фильтров в Assetic Readme.
Многие из этих фильтров передают задачу в другую программу или библиотеку, такие как YUI компрессор, так что вам, возможно, потребуется установить/настроить соответствующие библиотеки, чтобы использовать некоторые из фильтров.
Скачайте YUI компрессор версии 2.4.7 (обратите внимание, если вы будете скачивать компрессор из другого источника, версия компрессора 2.4.8 работать не будет), распакуйте архив и скопируйте файл yuicompressor-2.4.7.jar из папки build в каталог app/Resources/java/.
Примечание
У вас на компьютере должна быть установлена технология Java скачать можно по ссылке
Далее мы настроим Assetic фильтр для сжатия CSS с помощью YUI компрессора. Обновите конфигурацию приложения
# app/config/config.yml
# ..
assetic:
filters:
cssrewrite: ~
yui_css:
jar: "%kernel.root_dir%/Resources/java/yuicompressor-2.4.7.jar"
# ..
Мы настроили фильтр под названием yui_css, который будет использовать исполняемый файл YUI Compressor Java который мы разместили в каталоге ресурсов приложения. Для того чтобы использовать фильтр необходимо указать к каким assets вы хотите, чтобы применялся фильтр. Обновите шаблон чтобы применить yui_css фильтр
{# src/Blogger/BlogBundle/Resources/views/layout.html.twig #}
{# .. #}
{% stylesheets
'@BloggerBlogBundle/Resources/public/css/*'
output='css/blogger.css'
filter='yui_css'
%}
<link href="{{ asset_url }}" rel="stylesheet" media="screen" />
{% endstylesheets %}
{# .. #}
Теперь, если вы обновите страницу и просмотрите файлы вывода Assetic вы заметите, что они были минифицированы. В то время как минификация отлично подходит для рабочих серверов, это может осложнить процесс отладки, особенно когда сжимается JavaScript. Мы можем отключить минификацию при работе в окружении разработчика, предваряя фильтр с знаком вопроса ?
{% stylesheets
'@BloggerBlogBundle/Resources/public/css/*'
output='css/blogger.css'
filter='?yui_css'
%}
<link href="{{ asset_url }}" rel="stylesheet" media="screen" />
{% endstylesheets %}
Дамп assets для рабочего окружения
В рабочем окружении, Assetic генерирует пути к css и javascript файлам которые физически отсутствуют на компьютере. Но тем не менее, они выводятся, потому что внутренний контроллер Symfony открывает эти файлы и отправляет содержимое обратно (после выполнения применённых фильтров).
Этот вид динамической отправки обрабатываемых assets хорош, поскольку это означает, что вы можете сразу увидеть новое состояние любых файлов assets которые вы изменили. Минус в том, что это может происходить довольно медленно. Если вы используете много фильтров, этот процесс может замедлиться очень существенно.
К счастью, Assetic предоставляет способ сделать дамп assets в реальные файлы, а не генерировать их динамически.
Введите следующую команду чтобы создать дамп файлов asset.
$ php app/console assetic:dump
Вы заметите, что были созданы несколько CSS-файлов в web/css включая объединенный файл blogger.css. Теперь, если вы запустите веб-сайт symblog в рабочем окружении http://localhost:8000/app.php файлы будут обслуживаться непосредственно из этой папки.
Заметка
Если вы сделаете дамп файлов asset на диск и хотите вернуться к окружению разработчика, вам необходимо будет очистить созданные файлы asset в web/, чтобы позволить Assetic воссоздать их.
Вывод
Мы рассмотрели целый ряд новых областей Symfony2, включая окружения Symfony2 и как использовать Assetic asset библиотеку. Мы также усовершенствовали главную страницу и добавили некоторые компоненты на боковую панель.
В следующей главе мы перейдем к тестированию. Мы рассмотрим функциональное и unit тестирование. Мы увидим, что Symfony2 поставляется с несколькими классами, чтобы помочь в написании функциональных тестов, которые имитируют веб-запросы, позволяют нам заполнять формы, переходить по ссылкам, а затем проверять возвращаемый ответ.
Источники и вспомогательные материалы:
https://symfony.com/
http://tutorial.symblog.co.uk/
http://twig.sensiolabs.org/
How to Use Assetic for Asset Management
How to Minify JavaScripts and Stylesheets with YUI Compressor
How to Use Assetic For Image Optimization with Twig Functions
How to Apply an Assetic Filter to a Specific File Extension
Всем спасибо за внимание и замечания сделанные по проекту, если у вас возникли сложности или вопросы, отписывайтесь в комментарии или личные сообщения, добавляйтесь в друзья.
Часть 1 — Конфигурация Symfony2 и шаблонов
Часть 2 — Страница с контактной информацией: валидаторы, формы и электронная почта
Часть 3 — Doctrine 2 и Фикстуры данных
Часть 4 — Модель комментариев, Репозиторий и Миграции Doctrine 2
Часть 6 — Модульное и Функциональное тестирование
Также, если Вам понравилось руководство вы можете поставить звезду репозиторию проекта или подписаться. Спасибо.
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Комментарии (132)
M-A-XG
12.06.2016 13:40+1>Comments: {{ blog.comments|length }}
То есть, для того, чтобы показать количество комментов, нужно выбирать все комменты?
Гениально.
>Так как теги хранятся в базе данных в виде значений, разделенных запятыми
Явно нужна нормализация базы…
>if (false === is_null($limit))
Странное условие
>Гораздо лучший подход будет заключаться в том, чтобы отображать не дату, а время, которое прошло с момента публикации комментария
Очень спорно.
П.С.
Но все равно спасибо за руководство, так как скоро придется с этим чудом работать.padlyuck
12.06.2016 13:47+1M-A-XG
13.06.2016 09:11Это к чему?
calg0n
13.06.2016 09:51Это к тому что вы невнимательно читаете. В статье намеренно вызывается {{ blog.comments }} чтобы показать как работает ленивая загрузка и что не всегда это оптимальное решение её использовать.
M-A-XG
13.06.2016 10:22padlyuck какую-то дичь сует не в тему.
Нужно думать, что лениво грузить.
Грузить все комменты для показа количества не нужно ни лениво, ни нелениво.
Гении фреймворков собрались, блин.calg0n
13.06.2016 10:30Всё в тему. Вы похоже не читали книг по разработке в которых намеренно написан код в котором используется неоптимальное или неправильное решение, дабы читатель сам пошевелил мозгами в поисках правильного. Вы сами не до конца понимаете что и для чего используется, судя по вашей статье выше, но уже мните себя экспертом в фреймворках, хотя прочитали только про Yii, и то его не осилили.
Slavenin999
12.06.2016 17:22+1Загрузка всех комментариев для подсчета их количества — это выстрел себе в ногу. Комментариев может быть и 10000, но это не значит что нужно их все извлекать. Ни что не мешает вместо добавления их в выборку написать в селекте count(c.id) и сгруппировать по id блога. Ну и с тегами, странно хранить их через ","… Это должна быть м2м связь между записью блога и таблицей тегов, тогда можно аналогично считать теги, группируя их
postgree
13.06.2016 09:06Многие, кто работал с реальными нагрузками, вообще перестают выводить количество и просто расчитывают, необходимо ли показывать следующую страницу, или нет.
IvanPanfilov
12.06.2016 18:52-7> Создание блога на Symfony 2.8 lts [ Часть 5]
прямо сериал.
а могли просто поставить wordpressApollo286
13.06.2016 15:03Вы путаете цель и средство, в данном случае блог это не цель, а средство. Целью же тут является ознакомление с новым инструментом, на классической задаче, которая покрывает многие аспекты реальных задач из работы «Что-то типо web студии».
Сам когда-то знакомился с Django, на подобном материале, хотя и не написал в итоге ни одного блога :)IvanPanfilov
14.06.2016 19:32-1> Целью же тут является ознакомление с новым инструментом, на классической задаче, которая покрывает многие аспекты реальных задач из работы «Что-то типо web студии».
но wordpress покрывает 90% или более реальных задач из работы «Что-то типо web студии».
если б этот чудо фрймворк sympony показали на примере для чего то более серъезного — типа написания своего фейсбука. или гугола
я б дейсвительно задумался о его применении.
а так больше похоже на попытку усложнить и повысить стоимость разработки типовых задач. но зачем? если с этим справитс wordpress.zelenin
14.06.2016 19:49а молоток вы купите после того как им построют дом, или сколоченной табуретки будет достаточно?
Надо понимать, что написание фейсбука не зависит от фреймворка, поскольку 99,99999% кода к фреймворку отношения иметь не будет. А оставшиеся 0.00001% — реализуются на любом фреймворке одинаково.M-A-XG
14.06.2016 21:54>а молоток вы купите после того как им построют дом, или сколоченной табуретки будет достаточно?
Разжуйте, пожалуйста, свою аналогию, а то она не совсем понятна и очевидна. :)
>поскольку 99,99999% кода к фреймворку отношения иметь не будет.
Поскольку в здравом уме никто такой проект не будет писать на мейнстримовом фреймворке. Хотя фейсбук те еще клоуны.
Фейсбук на самописи?
IvanPanfilov
15.06.2016 09:05> написание фейсбука не зависит от фреймворка, *поскольку 99,99999% кода к фреймворку отношения иметь не будет*. А оставшиеся 0.00001% — реализуются на любом фреймворке одинаково.
и вот тут то мы и приходим к заключению — что фреймворки бесполезны.zelenin
15.06.2016 09:32+1я склонен считать, что для сильно кастомного проект полезен будет микрофреймворк (конфигурация, роутинг, вьюшки). фулл-фреймворк типа симфони тоже найдет свою нишу, поскольку из коробки покрывается большинство кейсов, что удобно для среднего уровня проектов.
Плюс фреймворки все-таки написаны людьми, прочитавшими «пару» книжек по проектированию приложений в отличии от людей, познающим мир фреймворков на примере 2008 года.
oxidmod
15.06.2016 09:32дело в том, что в конесте фейсбука — фреймворк это капля в море. и да, так как фреймворк призван быть универсальным, то достигается это путем неких компромисов (в частности компромиса с быстродействием). Тогда да, проще запилить собственный фреймворк идеально подогнаный конкретно под ваш проект. но это тоже компромис. он будет узкоспециализирвоанным, и не реюзабельным. пользоваться будет им только ваша компания. ну и сделать это можно лишь имея нормальный бюджет на разработку и время.
с другой стороны, для типичных приложений, ака ИМ, блог, доска объявлений с аудиторией в пару десятков-сотен тысяч пользователей фреймворк будет уже не каплей. он возьмет на себя львиную долю инфраструктурных вопросов и останется лишь закодить не слишком сложную в общемто бизнес логику.postgree
15.06.2016 09:59Для таких вещей существуют готовые решения. Тут уже спрашивали про вордпресс и т.д. и т.п. Если вы изначально делаете проект на мильёны пользователей с использованием фреймворка, то в итоге вы потихоньку будете пилить свои костыли и подпорки, и рано или поздно начнется поток негатива на первоначально принятое решение.
oxidmod
15.06.2016 10:23даже миллион пользователей не проблема для фреймворка. Более того, предпологаемый хайлоад лучше делать на фреймворке. готовые решения имеют предел прочности и их протюнить очень непросто. фреймворк же просто набор компонент. как вы их приготовите дело искючительно ваше. да и пробелма не в количестве пользователей, а в соотношении инфраструктурного кода и бизнес логики. когда инфраструктура лишь незначительный процент, то имеет смысл пилить её с нуля под себя. если вся бизнес логика — банальный круд, то смысл делать его руками, если фреймворк его генерит на автомате со схемы бд? железо дешевое, разработка дорогая.
postgree
15.06.2016 15:38Я где то утверждал, что для миллиона пользователей нужно брать вордпресс? Я всего лишь написал, что от фреймворка после конкретной тюнячки просто может остаться меньше 50%.
да и пробелма не в количестве пользователей, а в соотношении инфраструктурного кода и бизнес логики. когда инфраструктура лишь незначительный процент, то имеет смысл пилить её с нуля под себя. если вся бизнес логика — банальный круд, то смысл делать его руками, если фреймворк его генерит на автомате со схемы бд?
Признаюсь честно, не участвовал в проекте хотя бы на пару тысяч пользователей, где были бы тупо круды.
железо дешевое, разработка дорогая.
Как только дело начинает касаться денег, вы отказываетесь от шардинга и начинаете использовать мощные мешалки в HA или инструменты типа RAC. Машины под это дело довольно дорогие, и если есть возможность оптимизировать — необходимо оптимизировать. В недалеком прошлом закупал 8 сокетную конфигурацию о 64 ядрах в 2х экземплярах + хд + сеть. Мне бы, думаю, на (остаток жизни)x2 хватило бы. И это на x86.
На новой работе раз в 2-3 месяца приходится вбивать костыли. Да архитектура не ахти. От изначального ядра и фреймворка мало что осталось в процессе.oxidmod
15.06.2016 15:59нормальный фреймворк позволяет:
1) быстро стртонуть
2) по мере развития выбрасить то, что становиться узким местом и заменить более ефективной реализацией без переписывания всего и вся.
Готовое решение в разы трудней кастомизировать
Самопись на порядки медленней в стартеpostgree
15.06.2016 16:18Ок, приведите примеры «нормальных фреймворков».
Я работал на PHP только с Yii и CodeIgniter.
Готовое решение в разы трудней кастомизировать
Не повторяйтесь, я не спорю с этим утверждением.
Самопись на порядки медленней в старте
Моя работа не связанна со стартапами. Заказчику в итоге необходимо законченное решение, а не 20% для старта. Если нужно очень быстро стартануть, возьмите битрикс.oxidmod
15.06.2016 16:21+1>> Если нужно очень быстро стартануть, возьмите битрикс.
хуже совета не бывает
>> Ок, приведите примеры «нормальных фреймворков».
Я работал на PHP только с Yii и CodeIgniter
Yii2, Symfony, Laravel5
и для большей свободы парочка микрофреймворков:
Lumen, Silex
на худой конец композер и компоненты Symfony
VolCh
15.06.2016 12:07+1Костыли на проекте на мильёны пользователей начнутся скорее на вордпрессе или подобном решении, чем на фреймворке. Плюс хороший фреймворк позволяет органично (то есть без костылей и подпорок) заменить практически любую свою компоненту на стороннюю, включая самописную, задавая по сути только интерфейсы взаимодействия компонент. Та же Symfony в чистом виде по сути не фреймворк даже, а набор инфраструктурных компонент на многие случаи жизни. Фреймворком является, например, Symfony Standard Edition, где эти компоненты определенным образом объединены, но всегда можно заменить один компонент на другой с тем же интерфейсом, исключить ненужный, добавить нужный, изменить способы соединения всех используемых компонентов. Или написать свой фреймворк из готовых компонентов.
IvanPanfilov
15.06.2016 13:11> дело в том, что в конесте фейсбука — фреймворк это капля в море
тоже хороший пример бесполезности фрейморков.
wordpress.com пользуются милионы пользователей, там используется WordPress — который составляет не каплю в море, а именно что основу.
так же отсюда следует что WordPress вполне годится для высоконагруженных проектов.
> Тогда да, проще запилить собственный фреймворк идеально подогнаный конкретно под ваш проект. но это тоже компромис
> и сделать это можно лишь имея нормальный бюджет на разработку и время.
если сразу решать задачу на готовом инструменте, а не пилить свой фрейморк то и время и бюджет станет намного меньше.
PHP это специализированный язык для веб — поставляется с большим кол-вом функций из коробки.
Фреймворки всеголишь оборачивают всю имеющуюся функциональность в кривые абстракции
и не предоставлют никаких конкретных инструментов для решения задач.
данные абстракции в специализированных языках и не нужны вовсе (в PHP они как собаке пятая нога)
(они нужны лишь в языках типа C++ чтобы скрыть всю кривость данного языка и облегчить за счет этих костыльных абстракций программирование),
oxidmod
15.06.2016 13:43+1так может вы выложите на гитхаб свой фейсбук на вп?
любая разработка кастомного продукта сводится к написанию собственного фреймворка.
собственной реализации роутинга, собсвтенной абстракции над бд, собственными вьюхами и контроллерами, собственным контейнером. вопрос в том стоит ли писать свое, если есть готовый оттестирвоанный фреймворк, в котором еще и любую составляющую можно заменить по вкусу.
если для вас вп инструмент для серьезных проектов, то вам тем более не стоит пытаться писать свои реализации инфраструктуры приложенияIvanPanfilov
16.06.2016 19:46> собственной реализации роутинга, собсвтенной абстракции над бд, собственными вьюхами и контроллерами, собственным контейнером
непойму нах* это все нужно — если можно через PDO запросы фигачить, ну максимум написать свой кверибилдер для сохранения загрузки объектов/массивов в десяток срок кода и выводить данные встроеным в php шаблонизатором,
сохранять каждый экшн в файл. выделять основные функции в автоматически подгружаемые классы.
использовать роутинг apache или nginx.
я уже говорил выше что php не голый язык общего назначения, чтоб поверх него была необходимость городить якобы «удобные» абстракции
там уже из коробки все нужное есть.
но все развивается по циклу — и скоро люди осознают и примут эту истину.
сcылки на крупные проекты уже давал —
wordpress.com, www.bluehost.com/wordpress-woocommerceVolCh
16.06.2016 20:19Экшн в файл это хорошо. А как потом на него ссылаться с шаблонов, чтобы то же меню построить? Как параметры передавать, чтобы красиво было?
Шаблоны на голом пхп опасны, особенно если верстку на движок натягивает верстальщик с базовыми навыками js, вдруг прошедший курсі Попова.M-A-XG
18.06.2016 11:36>Экшн в файл это хорошо. А как потом на него ссылаться с шаблонов, чтобы то же меню построить?
О славянские боги! В чем проблема?
>Шаблоны на голом пхп опасны
Зато понятны всем!
Не берите на работу начинающих верстальщиков. Начинающие верстальщики эти ваши шаблонизаторы тем более не знают.
Разве в шаблонизаторах нельзя выполнить php код? В smarty легко. Даже без «тега» {php}VolCh
18.06.2016 13:58О славянские боги! В чем проблема?
Хардкод — всегда проблема.
Зато понятны всем!
Нет. Кроме того синтаксис пхп затрудняет понимание шаблонов.
Разве в шаблонизаторах нельзя выполнить php код?
В хорошем шаблонизаторе без дополнительных усилий — нет.
oxidmod
16.06.2016 21:53вы слишком узко мыслите.
представьте, что ваше приложение конектится не к одной, а к нескольким десяткам бд. Как вы будете управлять своими десятками pdo объектов?
представьте что ваше приложение — это не просто сайтик с десятком страниц, а а огромная црм, с сотнями и тысячами роутов… вы все еще хотите вручную прописывать правила на уровне вебсервера?
представьте, что ваше приложение крутится не на впс-ке за 5 баксов в месяц, а на ферме в несколько сотен серверов, в датацентрах по всему миру. Как вы будете править конфиги и рестартовать все эти машины?
а фоновые процессы вы тоже руками будете запускать?
Все эти «ненужные» абстракци позволяют в итоге снизить сложность проекта. Но вам это не понять, если вп и вукомерс ваши фавориты. Поищите статейку, как ребята тюнили свой проект на симфони под хайлоад (1 млрд запросов в неделю). сможете так же затюнить вукомерс?
зы. даже сам вам найду: http://labs.octivi.com/handling-1-billion-requests-a-week-with-symfony2/M-A-XG
18.06.2016 13:49>представьте, что ваше приложение конектится не к одной, а к нескольким десяткам бд
Это проблема?
Все разрулит построитель запросов.
>тысячами роутов
Шо вы курите?
>не на впс-ке за 5 баксов
У меня шаред хостинг держал 50К пользователей за 2 часа на самописи.
Новый рекорд уже на ВПС за 6 баксов:
115К пользователей в день, 240К хитов, 30К пользователей за час.
>на ферме в несколько сотен серверов
Какие фермы? Там никто не использует фреймворки (мейнстримовые).
>Все эти «ненужные» абстракци позволяют в итоге снизить сложность проекта.
Как по мне, только повышают.
VolCh
18.06.2016 14:04Шо вы курите?
Сотня сущностей и для каждой хотя бы 4 операции — уже почти полтысячи.
Как по мне, только повышают.
Хорошие абстракции — понижают, лишая необходимости думать о реализации. В документации к абстракции есть всё, что нужно для её правильного использования. Кстати, абстракции — краеугольный камень минимум двух принципов из SOLID — они все пять не нравятся или только два?M-A-XG
18.06.2016 17:59>Сотня сущностей и для каждой хотя бы 4 операции — уже почти полтысячи.
Это Вы типа REST API собрались строить?
Это псевдопрограммирование.
>Хорошие абстракции — понижают
Хорошие — да.
На фреймворках — сплошные дырявые.
Мне нравился Делфи с его библиотекой.VolCh
18.06.2016 23:42Это Вы типа REST API собрались строить?
Это псевдопрограммирование.
Это как псевдо?! Для вас программирование это шаблончики на сервере рендерить?
На фреймворках — сплошные дырявые.
Допустим, Symfony HttpFoundation Request — где дыры?
oxidmod
21.06.2016 09:48>> Это проблема? Все разрулит построитель запросов.
который написан самолично вами и судя по вашим комментам дыряв и неэфективен.
>> Шо вы курите?
на текущем проекте порядка 6.5к роутов
>> У меня шаред хостинг держал 50К пользователей за 2 часа на самописи. Новый рекорд уже на ВПС за 6 баксов:
115К пользователей в день, 240К хитов, 30К пользователей за час.
>> Какие фермы? Там никто не использует фреймворки (мейнстримовые).
а что если я скажу вам, что наш текущий проект на yii1 держит онлайн порядка 200к единовременных пользователей, а в часы пик до 400к. Да, от Yii там мало что осталось, но это всеже + в копилку фреймворка. Даже древняя версия в умелых руках не становится обузой, а помогает постепенно мигрировать части проекта на новые технологии и подходы.
>> Как по мне, только повышают.
Просто вы мыслите слишком узко))
M-A-XG
21.06.2016 10:12>что наш текущий проект на yii1 держит онлайн порядка 200к единовременных пользователей, а в часы пик до 400к
Опять какой-то сервис по выборке по первичному ключу? :)
Количество единовременных как меряете? Гугл аналитикой?oxidmod
21.06.2016 10:43>> Опять какой-то сервис по выборке по первичному ключу? :)
не поверите но да, большинство выборок по первичному ключу. Но не потому что сервис такой, а потому что еластик позволяет хорошо искать по всяческим параметрам))
есть и гугл аналитика, есть и собсвтенная статистика на базе графита и статсД
M-A-XG
18.06.2016 14:50В догонку.
Там нифига не написано, как они тюнили.
А большинство запросов там — по первичному ключу.
Это по ходу не сайт, а какой-то сервис по отдаче данных по первичным ключам. :)
30 ms — время генерации — много.
M-A-XG
18.06.2016 10:33Поддерживаю вас.
Конечно нужен строитель запросов.
+1 за роутинг на стороне вебсервера. Это его хлеб. Фреймворки проблему не там решают. Получаем оверхед.
VolCh
18.06.2016 14:05Нарушение инкапсуляции как минимум — веб-сервер должен знать о приложении очень многое, практически о каждой его функции.
M-A-XG
18.06.2016 18:59Зато можно отдельные разделы разнести на разные бекэнды :)
Нету оверхеда.
Не нужно разбираться, как строить роутинги, минус одно звено в программировании, система проще.
Что имеется в виду под «о каждой функции»?VolCh
18.06.2016 23:44+1С роутингом тоже можно, причём родным для веб-сервера способом — проксировать ссылки.
oxidmod
21.06.2016 12:24>> Зато можно отдельные разделы разнести на разные бекэнды :)
всеравно перед бекендом стоит балансировщикю он вполне может слать запросы на разные бекенды. Похоже вы ничего сложней бложиков и не делаете)M-A-XG
21.06.2016 13:23Мои проекты на самописи и так влезали в шаред хостинг :)
На работе на одном из проектов 10 бекэндов.
Но балансировщик — это не дело программистов.
Балансировщик шлет трафик не по разделам на разные бекэнды.
Обычно балансировщик работает на бекэнде с http-сервером.
А так можно обойтись без http сервера.VolCh
21.06.2016 16:35Балансировщик шлет трафик не по разделам на разные бекэнды.
Балансировщик (http) шлёт запросы как ему скажут. Скажем, запросы http://example.com/users он будет слать на один бэкенд, запросы http://example.com/posts распределять по десятку других бэкендов, а статику проксировать на CDN. И да, балансировщик, как и веб-сервер — это не дело программистов в общем случае. Но и дергать админов на каждый новый контроллер — очень быстро админы скажут, что это не их дело роутинг настраивать, будут требовать единую точку входа на приложение.
akubintsev
21.06.2016 11:57Рассмешили :)
Сейчас работаю в проекте под большой нагрузкой, где делали изначально роутинг на веб-сервере. Вышел такой некислый кусок nginx строк на 800. Прошло несколько лет, наняли SEO-оптимизатора. Интересно, вы представляете, сколько грабель было собрано на продакшене, пока отрубали банальный trailing slash в ссылках?M-A-XG
21.06.2016 12:221. А зачем отрубать trailing slash?
2. Для перевода url на без / можно написать 1 правило.
3. У меня нет в конфиге нгинкса завязки на trailing slash. Если она у вас была, то она может быть и в роутинге на приложении.
VolCh
16.06.2016 07:16+2и не предоставлют никаких конкретных инструментов для решения задач
Если вы имеете в виду бизнес-задачи, то в точку. Фреймворки — не инструменты для решения бизнес-задач, фреймворки — инструменты для создания инструментов решения бизнес-задач, обеспечивая типовые решения для типовых инфраструктурных задач. Лучше всего они себя проявляют, когда бизнес-задачи не типовые. Если ваша бизнес-задача — создание инструмента коммуникации с клиентами в виде стандартного блога, то писать блог на фреймворке не самое оптимальное решение в общем случае. Но если решили использовать готовое решение, то надо иметь в виду, что в случае если вам стандартного блога станет мало, то кастомизация готового решения в общем случае обойдётся дольше и(или) дороже чем если бы написали на фреймворке.M-A-XG
16.06.2016 12:43С таким успехом на самописи еще лучше :)
Правда, это скорее подойдет, если вы сами разбираетесь в программировании.VolCh
16.06.2016 19:09На самописи, как минимум, у вас будет очень дорогой и долгий старт.
M-A-XG
18.06.2016 20:02-2Распространенное заблуждение.
Никто ядро системы каждый раз с нуля не пишет.
Я бы и не был бы против фреймворков, если бы они были толковыми.
Но они упоротые и доставляют больше проблем, чем дают выгоды.VolCh
18.06.2016 23:45Никто ядро системы каждый раз с нуля не пишет.
Не пишут только те, кто используют фреймворки. Популярный или тот, о котором знает только автор — нюансы в контексте спора о том лучше писать с нуля или на фреймворке.
Apollo286
14.06.2016 20:09-1если б этот чудо фрймворк sympony показали на примере для чего то более серъезного — типа написания своего фейсбука. или гугола
я б дейсвительно задумался о его применении.
В общем-то, дальше уже не о чем говорить.
padlyuck
14.06.2016 23:12-1> я б дейсвительно задумался о его применении.
да вы не думайте, вам это вредно. продолжайте обмазываться своими самописями и всё у вас будет хорошо. главное намазывать толстым слоем.IvanPanfilov
15.06.2016 11:03> да вы не думайте, вам это вредно. продолжайте обмазываться своими самописями и всё у вас будет
WordPress самопись, лолчто?
~14 лет разработки и развития, сотни разработчиков ядра. c сохранением обратной совместмости,
а не переписыванием следующей мажорной версии с нуля как это любят делать во всех известных фреймворках.
Крупный коммерческий сервис wordpress.com с крупной коммерческой компанией https://en.wikipedia.org/wiki/Automattic
и многих других — есть кому вкладывать деньги в разработку и развитие, а значит это уверенность в будущем.
Вот просвещайтесь https://ru.wikipedia.org/wiki/WordPress
и еще вводная статья на хабре https://habrahabr.ru/post/282401/ для тех кто не в теме.oxidmod
15.06.2016 11:06>> написание фейсбука не зависит от фреймворка, *поскольку 99,99999% кода к фреймворку отношения иметь не будет*. А оставшиеся 0.00001% — реализуются на любом фреймворке одинаково.
> и вот тут то мы и приходим к заключению — что фреймворки бесполезны.
ага, тобишь вы предлагает хайлоад писать на вп… ну ок
padlyuck
15.06.2016 11:34с костылями и подпорками всё что угодно можно на всём чём угодно написать, только причем тут ваш вброс про вордпресс в теме о симфони?
з.ы. Самопись относилась не к вордпрессу, а к подходу некоторых товарищей «все фреймворки шлак, а я дартаньян»
baltazorbest
13.06.2016 09:08+1Спасибо за серию статей. Очень подробный и полезный материал. Единственно маленькая просьба, не могли бы Вы еще одной статьей рассмотреть построение блога на Symfony со стороны пользователей? Знаю что есть бандл FOSUserBundle но не уверен можно ли его использовать или есть какие то альтернативы лучше. Спасибо.
Winrol
13.06.2016 11:50+1Да, Плюсую за FOSUserBundle. Было бы интересно посмотреть на реализацию пользователей и админку(ее защиту).
wolframchegg
13.06.2016 11:50За такое:
{{ render(controller('BloggerBlogBundle:Page:sidebar' ))}}
нужно бить по рукам.M-A-XG
13.06.2016 11:57Почему? Мне нравится :)
tatenen
13.06.2016 15:04зачем дергать контроллер из вьюхи?
можно просто заинклудить вьюху с сайдбаром и все.padlyuck
13.06.2016 15:44+1а где делать выборки тегов и последних комментариев?
wolframchegg
13.06.2016 17:57Писать кастомную Twig функцию. Ибо дергание контроллера из вьюхи слишком затратно по производительности.
padlyuck
13.06.2016 18:47Спасибо за ответ. А форму добавления комментария тоже в таком случае лучше упаковать в отдельный "виджет" или это уже будет экономия на спичках?
wolframchegg
13.06.2016 23:37Форму комментария ненужно выносить в отдельный «виджет» ибо она будет не на каждой странице и ради нее не нужно дергать контроллер во вьюхе.
padlyuck
14.06.2016 00:15дело в том, что в предыдущей части статьи, форма добавления комментария выводится точно такой же конструкцией, так что в соответствии с данным мануалом контроллер по идее таки будет дергаться для вывода формы.
VolCh
13.06.2016 18:58+1Вообще это (дергать контроллер из вьюхи) стандартная рекомендуемая практика:
Whenever you find that you need a variable or a piece of information that you don't have access to in a template, consider rendering a controller. Controllers are fast to execute and promote good code organization and reuse. Of course, like all controllers, they should ideally be «skinny», meaning that as much code as possible lives in reusable services.
Symfony Book
Ваше «слишком затратно» в цифрах как-то выражается?wolframchegg
13.06.2016 23:46Когда вы дергаете контроллер из вьюхи то запускается весь kernel с его зависимостями и чем их больше, тем больше просядет производительность…
Symfony Book в свою очередь содержит информацию которая призвана популяризировать фреймворк, показать что не все так сложно, поэтому не всегда в нем описывают сложные подходы.
Ради интереса я когда то делал замер, данных для вывода было немного(облако тегов). С кастомной функцией гораздо быстрее рендерится страница.zelenin
14.06.2016 04:37+1Надо отметить, что весь кернел с зависимостями уже запущен, т.к. все это происходит в рамках одного инстанса.
Но чисто сементически мне это тоже не нравится.VolCh
14.06.2016 08:11+1Семантически, по-моему, вполне ок. Основной контроллер отвечает за предоставление основного контента в ответ на запрос пользователя, передаёт в представление только одну (ну, или какой-то минимум) переменную, непосредственно отображающие данные, запрошенные пользователем. Представление на базе каких-то факторов решает отображать или нет какой-то виджет, семантически несвязанный (мало связанный) с запросом/данными. Виджету нужны данные сервисов (включая ORM), виджету нужно своё представление, виджет может зависеть от прав пользователя — где как не в отдельном контролере всё это собирать и выдавать готовый html? Вполне себе HMVC-way. Плюс, если мы решим перенести рендеринг на сторону клиента, получая данные для него аяксом, то в контроллере нужно будет изменить формат вывода для него.
zelenin
14.06.2016 08:39>> где как не в отдельном контролере всё это собирать и выдавать готовый html
данные собирать сервисом, рендерить — вьюшкой, обернуть все это в некий компонент, не завязанный на реквест (в отличии от контроллера) — компонент слоя View (виджет yii, extension twig'а). Все-таки контроллер традиционно это что-то работающее с реквестом юзера.
Я понимаю HMVC как слоеную архитектуру с адаптерами в виде контроллеров между слоями. В php-фреймворках традиционно один контроллер, поэтому именно семантически это выглядит некорректно. То есть вынеси экшн, назови виджетом — и все довольны.psylosss
14.06.2016 09:43render(component(...)) это и есть вызов контроллера для получения некого ресурса. Да, у этого ресурса нет внешнего урла (а может и есть, никто не запрещает). Но он точно так же может работать с реквестом, с контейнером, генерить какие-то данные и выдаёт какой-то респонс. И это именно то, что требуется в сайдбаре. Получить ресурс "данные для сайдбара" — задача более подходящая для контроллера, нежели для твиг-функции (которая вобще-то не должна делать ничего, кроме обработки и подготовки к выводу уже поступивших данных, а тут она в БД полезет, начнёт с моделью работать).
zelenin
14.06.2016 09:53вот именно что render(component(...)) вызов компонента, а не контроллера. вопрос именно в семантике.
вызвать render('SidebarWidget'), который является инстансом некоего WidgetRenderer'а, семантически корректнее и srp'шнее, чем рендерить контроллер (хотя обе сущности могут функционально быть очень похожи).
Все мое имхо.psylosss
14.06.2016 10:00+1В симфони2 нет компонентов. Есть только контроллеры, которые получают реквест и выдают респонс. Компонент делает ровно то же самое — на входе название компонента (урл), на выходе респонс (html), тогда вопрос: чем компонент отличается от контроллера, и коль уже есть контроллер, то зачем нужен некий компонент? В симфони1 существование компонентов было оправдано тем, что они типа более легковесные, там не проверяются права и т.п. В симфони2, видимо, посчитали этот аргумент слишком незначительным, ну или были какие-то другие причины, я не знаю. При таком решении компонент-контроллер становится более независимым, а значит, более реюзабельным и тестируемым.
zelenin
14.06.2016 10:20компонент — это просто некая структура какой-то бОльшей структуры. Ничего конкретного я не имел в виду.
— контроллер получает данные (реквест), отдает json
— сериалайзер получает данные, отдает json
Является ли это описанием одинаковой сущности? Не отличаются ли они семантически, хоть и одинаковы функционально?
Контроллер рендерит всю страницу с помощью вьюшки. Виджет (отдельно формирующаяся часть вьюшки) рендерит часть вьюшки из вьюшки.
Экшн контроллера имеет урл, виджет не имеет.
Опять же протечка слоев — слой вьюшки запрашивает сущность из слоя контроллеров, хотя должно быть наоборот.psylosss
14.06.2016 10:30Все-таки не понимаю зачем вам нужна ещё одна структура под названием "компонент". Просто для того, чтобы показать, что она не самостоятельная структура, а вложенная? Ну называйте контроллеры SidebarComponent и всё :)
А если вам понадобится контроллер, который может быть доступен по урлу и может быть вставлен в виде "виджета", вы что будете делать? Ну например, форма логина, которая может быть в модальном окне и в body страницы, в зависимости от ситуации. Тут-то ваша "компонентная" логика и поломается.
Контроллер не обязан рендерить всю страницу, не пойму откуда этот стереотип. Он рендерит только то, за что отвечает (чаще всего — действительно за всю страницу целиком, но это не догма). И смысл разбиения страницы на несколько контроллеров — это делегирование ответственности.
Опять же протечка слоев — слой вьюшки запрашивает сущность из слоя контроллеров, хотя должно быть наоборот.
Вот именно. Вью говорит — у меня тут должен быть сайдбар, я хз что там за данные, нужны ли обращения к БД или не нужны, но вот ОН (контроллер) точно знает. Поэтому я сюда вставлю то, что ОН мне респонсит.zelenin
14.06.2016 10:50>> Все-таки не понимаю зачем вам нужна ещё одна структура под названием «компонент».
вы просто не смОтрите абстрактно на проблему. Если в симфони работает рендер контроллера из вьюшки, то это не значит, что оно верно. Я исхожу из того, что контроллер не должен таким образом рендерится, т.к. это не задача контроллера. Раз это не задача контроллера, значит, должен быть компонент, позволяющий это делать. Пусть это будет WidgetRenderer например. Ну SRP же.
>> А если вам понадобится контроллер, который может быть доступен по урлу и может быть вставлен в виде «виджета», вы что будете делать? Ну например, форма логина, которая может быть в модальном окне и в body страницы, в зависимости от ситуации. Тут-то ваша «компонентная» логика и поломается.
это другой кейс. Суть виджета: в провайдере данных и вьюшке, отображающей эти данные. Как я понимаю, new HtmlResponse((new SidebarWidget)->render()); без проблем отработает в симфони.
>> Контроллер не обязан рендерить всю страницу, не пойму откуда этот стереотип
>> Вот именно. Вью говорит — у меня тут должен быть сайдбар, я хз что там за данные, нужны ли обращения к БД или не нужны, но вот ОН (контроллер) точно знает.
Все остальное это как раз ваш стереотип, основанный на реализации виджетов в симфони.
То есть вы считаете, что в скобочках у «ОН» должно быть слово «контроллер», потому что привыкли так в симфони, а я считаю (вслед за классическим пониманием MVC из вики), что контроллер принимает данные от пользователя, и на основе бизнес-правил формирует вьюшку, а значит формирует ВЕСЬ выхлоп, а не часть. И дальше вьюшка ни с чем не общается — все свои проблемы в виде повторяющихся частей решает сама, в рамках своего слоя. Поэтому в скобочках у «ОН» должно быть все, что угодно, но только не контроллер.psylosss
14.06.2016 11:02Да поймите же, что в симфони2 нет разницы между компонентом и контроллером. И незачем её искусственно создавать. Во всех случаях, когда нужно проконтролировать генерацию вью, используются контроллеры. Частично или полностью, с урлом или без — это вы всё решаете каждый раз в частном порядке. Да, может быть в других системах штуки для генерации вью под названием "layout" называют "контроллерами", а штуки под названием "sidebar" называют компонентами. В симфони2 пошли другим путём. Контроллер контролирует генерацию вью. Если какому-то вью требуется для своего рендера другой вью, который контролируется кем-то, то он обращается к тому, кем оно контролируется, т.е. к контроллеру.
zelenin
14.06.2016 11:10>> Да поймите же, что в симфони2 нет разницы между компонентом и контроллером
еще раз: компонент — это часть некоей системы. Это не понятие из мира Симфони. Компонентом может быть сервис, контроллер, виджет, рендерер, репозиторий, валидатор и так далее. Как видите видов компонентов много, а отличаются они — своей ответственностью.
$postService->findById($id) — возвращает Post
$postRepository->findById($id) — возвращает Post
Функционал и сигнатура одинаковые, а семантика и ответственность этих двух компонентов разная.
Должен быть компонент, рендерящий вьюшку на основе клиентского реквеста. Должен быть компонент, рендерящий часть вьюшки для реюзабельности. Две ответственности — два компонента.
Обоснование тем, что в симфони так, нерабочее, поскольку эта ветка началась именно с некорректности использования контроллера в качестве виджета.psylosss
14.06.2016 11:23Контроллер это тоже часть некой системы, и в чем отличие от компонента? Реквест — это всего лишь аргумент контроллера (он там может быть, а может и не быть).
$postService->findById($id) — возвращает Post
$postRepository->findById($id) — возвращает Post
Вот смотою и туплю, а зачем нужен $postService? Есть же репозиторий для этих целей.
Должен быть компонент, рендерящий вьюшку на основе клиентского реквеста. Должен быть компонент, рендерящий часть вьюшки для реюзабельности. Две ответственности — два компонента.
Оба на входе принимают аргументы, а на выходе — респонс для вью, и в этом заключается их ответственность, которая одинаковая в обоих случаях. Симфони не стал делить контроллеры на внутренние/внешние, вложенные/невложенные, с реквестом/без него. Контроллер должен выдать респонс. Если тебе нужен респонс, то обращайся к контроллеру. Всё остальное — повышение уровня абстрации.zelenin
14.06.2016 11:39>> Контроллер это тоже часть некой системы, и в чем отличие от компонента?
ни в чем. Контроллер — один из видов компонентов, принимающий клиентский реквест.
>> Реквест — это всего лишь аргумент контроллера (он там может быть, а может и не быть).
аргумент метода — это реализация на уровне языка. Реквест — это данные, присланные клиентом.
Я выше давал пример:
— контроллер получает данные (реквест), отдает json
— сериалайзер получает данные, отдает json
реализация одинаковая — смысл разный. Поэтому две разные сущности. Повторю десятый раз: SRP, семантика.
>> Вот смотою и туплю, а зачем нужен $postService? Есть же репозиторий для этих целей.
разные слои. репозиторий — слой хранилища и отдаст мне запись, если она есть. сервис проверит мои права на получение данных например, и если права есть, получит запись из репозитория.
Аналогично контроллер возвращает html, хотя на самом деле html возвращает HtmlResponse внутри контроллера.
>> Контроллер должен выдать респонс. Если тебе нужен респонс, то обращайся к контроллеру.
респонс может выдать все что угодно. Опять же сужаете. Если существует что-то, что выдает респонс, это не значит, что респонс не может выдать что-то другое. У контроллера нет прерогативы на использование респонса.
>> Оба на входе принимают аргументы
аргументы метода — это синтаксис языка. У любого метода могут быть аргументы, и это не может быть доказательством чего-либо из обсуждаемой нами плоскости.
А вот смысл этих аргументов может повлиять на многое (см. пример контроллер/сериалайзер).
Вообще мы все это обсуждаем в контексте SRP или в контексте «так уже работает — сойдет»? Из второго принципа я уже вырос — стал плюсы solid чувствовать кожей, а нюансы семантики вижу невооруженным взглядом.psylosss
14.06.2016 11:51Кажется, я понял в чем ваше затруднение. Вы зачем-то наделяете вещи (контроллеры, сервисы) знаниями о том, кто, откуда и зачем их вызывает. Они не должны этого знать. Контроллеру должно быть всё равно, откуда он вызывается. Ему дают аргументы (реквест, объект, что угодно) и он на основании этих аргументов строит респонс. Его не должно волновать, вызывается он функцией из вью, из kernel или ещё откуда-то.
Конечно, сервис $postService имеет право на жизнь, но не проще ли воспользоваться уже готовым кубиком voters? Соединяем repository+voter+controller и получаем ваш postService. SOLID говорите? Ну-ну. И достать, и права проверить...
И я понял как вы решаете эту проблему. Вы просто делаете более высокоуровневую надстройку над фреймворком, чтобы вам было удобнее. Увеличиваете размер кубиков. Что ж, такая стратегия имеет право на жизнь, но тогда почему симфони? Почему не Yii или не битрикс с вордпрессом? Там это уже в готовом виде.
zelenin
14.06.2016 12:05>> Кажется, я понял в чем ваше затруднение. Вы зачем-то наделяете вещи (контроллеры, сервисы) знаниями о том, кто, откуда и зачем их вызывает. Они не должны этого знать. Контроллеру должно быть всё равно, откуда он вызывается. Ему дают аргументы (реквест, объект, что угодно) и он на основании этих аргументов строит респонс. Его не должно волновать, вызывается он функцией из вью, из kernel или ещё откуда-то.
правильно я понимаю, что если у меня есть PostSerializer, но нет PostController, то я должен реквест закинуть в PostSerializer, ведь результат будет одинаков? Кто первый тот и отдает json?
Одно и то же повторяю разными словами по кругу.
>> Конечно, сервис $postService имеет право на жизнь, но не проще ли воспользоваться уже готовым кубиком voters?
абстрагируйтесь от симфони. Я не ищу решения какой-то проблемы в своем проекте на симфони — я с вами о программировании с т.з. правильности решений.
>> SOLID говорите? Ну-ну. И достать, и права проверить…
а в чем у вас проблема с этим? вообще понятие сервисов (не сервисов симфони) сервисного слоя вам знакомо?
>> И я понял как вы решаете эту проблему. Вы просто делаете более высокоуровневую надстройку над фреймворком, чтобы вам было удобнее. Увеличиваете размер кубиков. Что ж, такая стратегия имеет право на жизнь, но тогда почему симфони? Почему не Yii или не битрикс с вордпрессом? Там это уже в готовом виде.
вот это поворот. В каком месте я надстройку то сделал? я наоборот ухожу в сторону низкоуровневости, гибкости и развязаности, создавая сущность со своей ответственностью, вместо использования контроллера, где ни попадя, лишь потому что он может вернуть html.
Мы явно говорим на разных языках. Я не хочу переходить на личности, но посоветовал бы почитать книг вообще по архитектуре приложений, какие-нибудь энтерпрайз шаблоны Фаулера, ДДД Эванса. В вас чувствуется «специалист одного фреймворка», обосновывающий все конкретной реализацией, а не логичностью.psylosss
14.06.2016 12:26правильно я понимаю, что если у меня есть PostSerializer, но нет PostController, то я должен реквест закинуть в PostSerializer, ведь результат будет одинаков? Кто первый тот и отдает json?
Одно и то же повторяю разными словами по кругу.
Если у вас по какой-то неведомой причине PostSerializer возвращает объект Response, то да. Но очевидно что это не так, поэтому реквест надо закидывать в PostController.
а в чем у вас проблема с этим? вообще понятие сервисов (не сервисов симфони) сервисного слоя вам знакомо?
У меня как раз с этим никаких проблем нет :)) Есть механизмы, отвечающие за извлечение записей из БД, есть механизмы, отвечающие за проверку прав. Вы повысили уровень абстрации, объединив их в один механизм, который назвали postService.
Мы явно говорим на разных языках. Я не хочу переходить на личности, но посоветовал бы почитать книг вообще по архитектуре приложений, какие-нибудь энтерпрайз шаблоны Фаулера, ДДД Эванса. В вас чувствуется «специалист одного фреймворка», обосновывающий все конкретной реализацией, а не логичностью.
Что ж, на этом и предлагаю закончить эту увлекательную дискуссию. Благодарю.zelenin
14.06.2016 12:35>> У меня как раз с этим никаких проблем нет :)) Есть механизмы, отвечающие за извлечение записей из БД, есть механизмы, отвечающие за проверку прав. Вы повысили уровень абстрации, объединив их в один механизм, который назвали postService.
в вашем программировании так не делают? Ваш контроллер случайно не извлекает ли из БД данные и не рендерит ли вьюшку?
Вы ведь знаете про толстые контроллеры?
>> Если у вас по какой-то неведомой причине PostSerializer возвращает объект Response, то да. Но очевидно что это не так, поэтому реквест надо закидывать в PostController.
контроллер должен вернуть строку, выводимую в браузер, консоль, выхлоп http-клиента. Остальное — вопрос реализации. Но собственно ответа вашего это не меняет. Теперь ваш сайт стал первым сайтом, у которого вместо контроллера сериалайзер.
Ок. Тогда закругляемся.VolCh
14.06.2016 16:54Ваш контроллер случайно не извлекает ли из БД данные и не рендерит ли вьюшку?
Именно контроллер должен привести модель в состояние готовности к обработке запроса от view. Именно контроллер создаёт представление и связывает его с моделью. Это основная ответственность контроллера — определить какой-метод какого объекта модели дернуть (при необходимости его создав или восстановив из хранилища), дернуть этот метод, определить какому представлению (при необходимости его создав) сообщить о результате (при необходимости его создав) и собственно сообщить.
psylosss
14.06.2016 11:29Кстати, вот подумал, если вам очень хочется иметь компоненты (мне тоже хотелось, когда я слезал со своего велосипедного фреймворка на симфони), то их можно реализовать с помощью всё тех же мастер-реквестов. Ну и психологическую обёртку над render(controller(...)), например,
render_component('sidebar')
:)zelenin
14.06.2016 11:55еще раз: компонент — общее понятие, не имеющее отношение к симфони. Симфони как и любой фреймворк состоит из компонентов различного уровня и предназначения. Я не хочу иметь компонент — я хочу, чтобы каждая сущность занималась своим делом. Аргументация вида «в симфони так» не работает.
psylosss
14.06.2016 11:56Контроллер генерит респонс. Он занимается только этим. В чём вопрос?
zelenin
14.06.2016 12:12serializer возвращает json, контроллер возвращает json — зачем две сущности? вопрос в этом.
psylosss
14.06.2016 12:27Контроллер возвращает не JSON. Контроллер возвращает Response (JsonRespose, извините что я опять про симфони).
zelenin
14.06.2016 12:41строку, генерируемую респонсом. Вы же в браузере строку видите.
https://github.com/symfony/http-foundation/blob/master/Response.php#L236
опять вы реализацию поставили выше сути. Что бы ни возвращал мой контроллер (в любой фреймворке), итогом будет строка, во чтобы она ни была обернута для целей инкапсуляции — хотя для вас это «кубик» и повышение абстракции, ведь можно просто в контроллере вывести все напрямую.
psylosss
14.06.2016 11:07И да, в симфони также нет такого понятии как "страница", "вложенная структура" и т.п. Есть контроллеры, есть роуты, есть шаблоны. Все эти дополнительные абстракции, которые делают фреймворк более высокоуровневым, присущи другим продуктам, типа Yii. Вероятно, они даже в некоторых местах делают разработку удобнее. Но всё же, если нужен блог, то решения лучше вордпресса не найти. Симфони2 удивительный фреймворк. Он задаёт стандарты, предоставляя при этом чрезвычайно маленькие и несвязанные кубики. В Yii-подобных этого даже близко нет и не предвидится. Монолит, чрезвыйчайно кастомизируемый и чуть более современно написанный битрикс.
VolCh
14.06.2016 16:42В классическом понимании MVC связь view:controller не один-к-одному, а многие-ко-многим, если рассматривать контроллер как обработчик единичных запросов от представления, а не всю их совокупность.
psylosss
14.06.2016 09:33Измеряли, само собой, в prod? Каким образом? "Гораздо быстрее" — это насколько в процентах?
Кстати, говорят, на голом пхп ещё быстрее страницы рендерятся. Но фреймворк не только и не столько для скорости исполнения кода придуман. Если есть какая-то отделяемая многократно используемая часть шаблона, то для этого можно и нужно использовать render(controller(...)). А не наделять вью (да-да, кастомная функция твига это вью) несвойственными ей полномочиями.
wolframchegg
14.06.2016 09:59Измерял не в prod режиме, мне важнее было понять что быстрее отработает. В дев режиме отрендерило быстрее в 4 раза.
psylosss
14.06.2016 10:03Ну вы же понимаете, что это не показательно… Быстрее в 4 раза? Дело однозначно не в контроллере/твиг-функции, там просто неоткуда взяться такой разнице. Скорее всего вас с контроллерами что-то не то. Если у вас висят обработчики на события реквеста, проверьте, нужны ли они на ВСЕ реквесты, или же стоит ограничиться мастер-реквестами. Почти уверен, что дело в этом.
M-A-XG
14.06.2016 14:19>Symfony Book в свою очередь содержит информацию которая призвана популяризировать фреймворк, показать что не все так сложно, поэтому не всегда в нем описывают сложные подходы.
Так в чем я не прав?
>Документация во всех фреймворках показалась какой-то попсовой, вроде расчитаной на дебилов (http://blog.kpitv.net/article/frameworks-1/ )
psylosss
14.06.2016 09:24Очевидно, с такой позицией начнётся драка. У меня есть желание бить по рукам тем, кто запрещает такое по неведомым причинам.
wolframchegg
14.06.2016 10:02Причина описана выше. Да никто не запрещает юзать render контроллера, как и юзать lazyLoad для вывода данных из связных таблиц. При малых обьемах данных разница будет минимальна, а вот при больших…
psylosss
14.06.2016 10:07Озвученная причина несостоятельна. А вот с lazy load прикол (проблема) в том, что при больших количествах зависимостей между выбираемыми сущностями иногда бывает быстрее (даже в продакшене) использовать единичные доп.запросы. Потому что гидрация данных начинает сжирать какое-то невероятное время. И бывает быстрее выбрать только блоги+пользователей, а потом маленькими быстрыми запросами дополнить их телефонами+оценками+Х+Y+Z, чем выбрать сразу всё одним запросом…
postgree
14.06.2016 12:10Такое делается только для отношений один ко многим. Все, что идет один к одному лучше выбирать одним запросом. Для уменьшения трафика и кейса, когда никакая часть данных не может быть изменена, дополнительно указываются поля для выборки.
psylosss
14.06.2016 12:30Всё верно, блоги+пользователи это один-к-одному, а телефоны+оценки+X+Y+Z это и есть один-ко-многим, поэтому и разделяем. Вот и приходится извращаться денормализацией и отдельными запросами. Да, проблема именно в том, что result set слишком большой, даже если в итоге всего 10 постов на странице
M-A-XG
14.06.2016 12:26-1А-ха-ха.
Подрались и без меня.
А об этом я писал в своей статье:
«Сам подход, что запускается 1 контроллер глуп.»
«Нету возможности выполнить другой контроллер.»
Фреймворки явно с подходом «только один контроллер» для хоумпаджей.
В моей самописи «контроллеры» легкие. :)oxidmod
14.06.2016 12:30с учетом повального перехода на SPA 1 контроллер на 1 реквест — очень даже правильно
BoShurik
15.06.2016 13:16Тоже сначала так думал, но потом понадобилось сделать так:
{{ render_esi(controller('BloggerBlogBundle:Page:sidebar' )) }}
padlyuck
В оригинальной статье допущена ошибка. Этот код:
не будет корректно возвращать посты при заданном лимите, из-за того что к таблице постов джойнятся комментарии.
psylosss
Точно! Распространённые грабли, в том числе среди не новичков. Поясню для тех, кто знакомится с ними впервые: чтобы получить то, что требуется ($limit блогов с комментариями), нужно сначала (отдельным запросом) выбрать айдишники нужных блогов в количестве $limit штук, а потом (вторым запросом) выбрать блоги, комментарии к ним и всё что ещё потребуется, используя
->andWhere('b.id IN :blogs_ids')
, при этом не используя ниsetMaxResults
, ни всякиеandWhere
. Бандлknp_paginator
берёт эту рутину на себя, его можно использовать не только для пагинации.M-A-XG
Я ж говорю, что фреймворки — это сплошные дырявые абстракции:
http://blog.kpitv.net/article/frameworks-1/
:)
psylosss
Бред какой-то по ссылке. Даже комментировать не хочется. Не знаю кто автор, но это у него в голове сплошные дырявые абстракции.
padlyuck
продолжаете размахивать пиписькой? Прекращайте. В предыдущей части вы уже порядком показали что плаваете в вопросах практической реализации современных фреймворков. Иначе говоря ваши аргументы устарели.
IvanPanfilov
> что плаваете в вопросах практической реализации современных фреймворков
бхахах.
зато в практическом решении задач не плаваем. в отличии от любителей обмазыватся фрейморками.
padlyuck
M-A-XG перелогиньтесь
oxidmod
тролль голодает, тролль ищет пищу, троль приспосабливается
calg0n
Ода человека который не смог :) Эта статья мне напоминает то, как если бы вы взяли в руки молоток и уронили его себе на ногу, а потом всем рассказывали что молоток — плохой инструмент и вообще все молотки — это кошмарные инструменты :)
MetaDone
не забывайте пожалуйста к своей статейке прикладывать ссылку на свои исходники
M-A-XG
Я думал, Вы сами бросите ссылку на статью и на код. :)
Почему я до сих пор должен сам постить ссылку на статью?
oxidmod
прекращайте кормить троля
padlyuck
поделитесь пожалуйста примером как это красиво реализовать на симфони именно для ситуации когда нужно вывести несколько постов с лимитом, и комментарии к ним. В Yii2 есть магический метод with(), а в doctrine пока ничего подобного не нашел(возможно и не найду?)
psylosss
Красиво — на мой взгляд, с помощью knp_pagination. Доктрина1 умела самостоятельно делать то, что вы от неё ожидаете, доктрина же вторая, видимо, пошла более нативным путём. К тому же никто не мешает реализовать свой
with()
для доктрины.