Для профессионалов в тестировании! >>>TestingStage2018>>> Продажа билетов на конференцию открыта. Успей купить!
×Закрыть

10 вещей, которые я хотел бы знать перед стартом проекта на Symfony

Прошло уже около двух лет с тех пор, как мы начали переписывать наш проект на Symfony2. Это было интересное время — работа с Symfony2+Doctrine2 была настоящим удовольствием после Битрикса. Конечно, оглядываясь назад, я понимаю, как не надо было делать, и сегодня делюсь этим с вами.

1. Не добавляйте файл composer.lock в .gitignore

Если вы пишете на Symfony, то наверняка используете Composer. Если нет — значит, что-то у вас в самом начале не заладилось.

Про странную манипуляцию с файлом composer.lock как-то говорил Рома Лапин на конференции PHP Frameworks Days в Киеве. Напомню, что файл composer.lock создается после последней успешной установки зависимостей и хранит последнее стабильное дерево зависимостей вашего приложения в точности до коммита. Это означает, что с помощью команды «composer install» вы можете откатиться до последнего стабильного состояния.

Многие разработчики почему то считают, что файл composer.lock лучше заигнорить. Мы не были исключением и сделали это, потому что в один прекрасный день у нас в зависимостях что-то поломалось — то ли при переходе на Symfony 2.3 (LTS), то ли при смене версии компонента symfony/icu.

Почему не надо? Может получиться забавная ситуация, когда одновременно у вас, вашего коллеги и на сервере окажется совершенно разный код с разными коммитами — и вполне возможно, приложение в каком-то месте будет работать некорректно.

2. Придерживайтесь слабой связанности бандлов и сервисов

Фреймворк Symfony построен так, чтобы дать разработчикам очень высокую степень гибкости в разработке и конфигурировании веб приложения. Symfony позволяет буквально жанглировать сервисами и зависимостями.

Одна из вещей, которую мы, на мой взгляд, сделали неправильно — пошли по пути жесткой связности бандлов, в то время как фреймворк предполагает другой подход.

История была примерно такая:

Есть популярный бандл SonataAdminBundle. Предполагается, что вы будете размещать admin-классы в своих бандлах примерно так:

src/Company/BlogBundle/
    Admin/
        ArticleAdmin.php
        CommentAdmin.php
   Entity/
   Resources/
       Config/
           admin.xml
   ...
src/Company/UserBundle/
    Admin/
        UserAdmin.php
        GroupAdmin.php
   Entity/
   ...

Преимущество этого подхода в том, что если вы добавляете в свой проект бандл, который использует SonataAdminBundle, достаточно просто добавить одну строку в app/AppKernel.php — и все ваши админ классы начинают работать, и новые сущности становятся доступны для редактирования в админ-панели.

Но мы, тогда еще непросвещенные, подумали: «Да ну, залазить в каждый бандл по отдельности и править админ классы и сервисы — это странно и неудобно!» — и решили, что будет лучше завести для этого новый бандл src/Company/AdminBundle для всех админ классов и сервисов:

src/Company/AdminBundle
    Admin/
        ArticleAdmin.php
        CommentAdmin.php
        UserAdmin.php
        GroupAdmin.php
        ...Admin.php
    Resources/config/admin.xml

В общем-то, на первый взгляд, тут ничего страшного и нет. Но впоследствии мы пожалели об этом, и вот почему.

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

Мы приняли решение разделить фонд и медиатеку на разные сервисы. Нам теперь важно, чтобы эти сервисы были в разных git-репозиториях и работали на разных инстансах.

Сейчас я понимаю, что это будет непростая задача. Если бы мы в самом начале шли по пути слабой связности бандлов и сервисов, было бы намного проще.

3. Не плодите сущности

В Doctrine очень легко поддаться искушению лепить сущности везде, где надо и где не надо. Иногда это происходит по незнанию, иногда — по неопытности. Далее расскажу об одном из самых неудачных решений, которое очень испортило мне дальнейшую разработку.

«Произведение» в нашей медиатеке — это одна из главных сущностей, которая агрегирует в себе аудио, видео и книги. Однажды нам понадобилось добавить свойство «тип произведения»: аудио, видео, книги, музыка. Это свойство используется в поиске и в админке для фильтрации.

Главный нюанс состоит в том, что должна быть возможность фильтровать произведения одновременно по нескольким типам — например, отобразить те, у которых проставлен тип «аудио» и «книги».

Сразу стало понятно, что одним свойством тут не отделаешься. Мы решили добавить сущность Type и связать ее с произведением ассоциацией ManyToMany. Теперь, чтобы понять, какой тип проставлен в произведении, нам приходилось каждый раз перебирать типы через метод getTypes(). Также для удобства пришлось завести методы типа hasAudio() чтобы проверять, проставлен ли в произведении нужный тип.

Вызов метода getTypes() означал, что Doctrine будет делать запрос в базу данных, и это очень напрягало. Более того, пришлось также заводить фикстуры, чтобы при разворачивании проекта создавались эти типы. Страшно вспоминать :)

Немного позже, когда мне всё э то сильно надоело, я убрал эти странные сущности и просто добавил четыре булевых свойства audioType, videoType, bookType, musicType. Жить сразу стало легче.

Но одно такое неправильное решение сильно портило нам жизнь на протяжении длительного времени. Если вдруг вам в голову придет мысль, чтобы решить такую же проблему нашим способом, — гоните ее куда подальше!

4. Можно ли ваш проект развернуть в любом месте, быстро и без боли?

Это чем-то похоже на порядок в доме. Его можно либо поддерживать, либо в один прекрасный день ужаснуться тому, как быстро ваш дом стал похож на сарай, закатать рукава и пару дней заниматься уборкой.

Когда процесс разработки длится месяцы, очень легко перестать думать о том, что код проекта захочет запустить кто-то, кроме нас, или что нам самим придется развернуть проект в другом месте.

Часто бывало, что мы писали какой-то код, деплоили его на сервер, и всё работало. Но когда по какой-то причине приходилось разворачивать проект с нуля, внезапно начинали появляться какие то ошибки или эксепшены — и мы получали нерабочее приложение.

Чаще всего проблема возникала от того, что мы добавляли в базу данных какие-то служебные записи, но забывали добавить их в фикстуры или в скрипт установки проекта.

Плохо, когда для развертывания приложения нужно загрузить какие то данные в базу. Чаще всего такая проблема у нас возникала от того, что кто-то забывал добавить файл миграции в GIT, и потом другой получал mysql ошибки и нерабочий сайт.

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

А есть ли возможность всегда поддерживать код приложения в порядке? Что делать, чтобы проект всегда можно было быстро развернуть в любом месте? Ответ оказался прост:

5. Используйте сервер непрерывной интеграции (CI)

Автотесты и сервер непрерывной интеграции — это темы, которые в последнее время не обсуждает только ленивый. Можно долго спорить и рассуждать на тему, нужны тесты или нет. Мы для себя решили, что нужны. Как минимум — Behat, а как максимум — PHPUnit и JS.

Самое большое преимущество CI в том, что мы можем автоматически запускать все тесты после каждого пуша в репозиторий и видеть, чей коммит всё поломал. Это особенно здорово, потому что спасает нас от деплоя бажного кода.

Кроме этого, благодаря CI вы можете поддерживать код постоянно в таком состоянии, чтобы в любой момент быстро и безболезненно развернуть приложение. Вы можете настроить всё так, чтобы сервер скачивал из репозитория последнюю версию кода, разворачивал проект на тестовом сервере и прогонял все тесты. CI на любом этапе скажет вам о проблеме, если что-то пойдет не так. Если вы сразу заметите проблему и исправите ее, то облегчите себе жизнь в будущем.

Мы остановили свой выбор на Jenkins. Настроили его таким образом, что после каждого пуша в GitHub Jenkins скачивает код последнего коммита и запускает скрипт bin/jenkins_scenario, который:
— С помощью Composer устанавливает все зависимости для проекта;
— Создает базу данных;
— Запускает миграции;
— Загружает в базу данных фикстуры;
— Запускает тесты.

6. Не игнорируйте JavaScript тесты

Этот пункт касается не только Symfony проектов. Тем не менее, сейчас я жалею о том, что мы пока так и не начали писать JavaScript тесты и не прикрутили их к Jenkins.
Behat и PHP Unit-тесты — это очень хорошо, но подчас JS тесты могут оказаться не менее важными.

Например, была ситуация, когда мы прицепили обработчик Google Analytics на кнопку для совершения пожертвования на сайте (для подсчета конверсий), но как следует не протестировали ее работу. В результате пользователи длительное время не могли жертвовать деньги, потому что эта кнопка просто не работала. Проблема обнаружилась только на следующее утро. Получилась неприятная ситуация, из-за которой фонд не дополучил деньги.

Если бы CI каждый раз запускал и JS тесты, то еще на этапе Pull Request в GitHub мы увидели бы красивое сообщение о том, что сборка провалилась, и не деплоили бы такой серьезный баг на сервер.

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

7. Не пишите в контроллерах служебные вещи — парсеры, скрипты для переноса данных и тому подобное

Нам часто нужно было переносить данные между старым и новым сайтом. Иногда приходилось писать на старом сайте скрипт, который отвечал бы на запросы в JSON формате, а потом в цикле слать запросы с нового сайта, чтобы получить информацию.

Мы решили для таких целей использовать контроллер с торчащим наружу URI типа «/move/authors». Мы просто открывали в браузере URI «/move/authors», и скрипт делал перенос.

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

К какому выводу мы пришли?
Лучшее решение — использовать для таких целей компонент Symfony Command. Просто запускаете на сервере нужную команду типа php app/console demo:dosomething — и ничего не будет торчать наружу.

Ещё один интересный момент, который я заметил: PHP-процесс, запущенный таким образом, работает стабильнее, чем вызов URI сайта через браузер.

8. Как вы делаете деплой?

В самом начале мы не особо беспокоились о том, как будем доставлять код на сервер. Когда он попал на Git, мы просто начали использовать для обновления команду git pull.

Конечно, для деплоймента Symfony приложения этого недостаточно. Ведь нам нужно каждый раз:
— Загружать код проекта из главного репозитория;
— Запускать миграции;
— Чистить/прогревать кеш;
— Устанавливать новые composer зависимости;
— Делать дамп css/js файлов;
— ...

Дальше список можно продолжать, всё зависит от нужд проекта. В самом начале мы каждый раз заходили на сервер и запускали все эти команды вручную. Самое интересное — забыть одну из них, и после долго выяснять, почему ничего не работает!

Однажды фонд принял решение найти еще одну команду разработчиков, которая бы занялась переписыванием одного из проектов на Symfony.

Когда мы начали плотно сотрудничать с этими парнями, они нас познакомили с Capifony. Это замечательный инструмент деплоймента, построенный на основе Capistrano, кастомизированный для Symfony приложений.

Несмотря на то, что установить Capifony в ubuntu/windows непросто, он может сильно упростить вам жизнь: для деплоя достаточно одной лишь команды, запущенной в терминале — cap deploy.

9. Будьте прагматичны

Представьте себе связь один-ко-многим. Один Автор может иметь много Произведений, при этом Произведение может не иметь Автора.

Route для произведения с автором выглядит так: /dostoevskiy-fedor-mihaylovich/idiot/

Без автора — так:
/bez-avtora/skazki-i-skazanija/

Генерируя путь к произведению, каждый раз придется проверять, есть ли у произведения автор, и исходя из этого выбирать нужный Route. Кроме этого, в проекте есть много страниц, в которых также должна быть ссылка на произведение, а значит, надоедливых if/else не избежать.

Однажды мой коллега предложил: «Слушай, а давай создадим „неизвестного автора“, и к нему привяжем все произведения без автора — таким образом нам будет намного легче работать со связями, роутингом и так далее?».

Мне эта идея не понравилась. Я тогда подумал: «придется писать костыли, чтобы этого неизвестного автора не показывать в списках...» Так как последнее слово было за мной, еще раз подумав, я отказался. И сейчас сильно жалею об этом. На каждом углу при разработке нам приходится делать небольшие «танцы с бубном» и писать проверки типа:

{% if audio.work.author %}
   <p>
        <b class="book-col-title">Автор: </b>
        <a href="{{ path('author_show', { 'seoCode': audio.work.author.seoCode }) }}">
            {{ audio.work.author.name }}
        </a>
    </p>
{% endif %}
{% if audio.work %}
    <p>
        <b class="book-col-title">Произведение: </b>
        <a href="{{ path('work_show', {
        'authorSeoCode': audio.work.author ? audio.work.author.seoCode : unknown_author,
        'seoCode': audio.work.seoCode
        } ) }}">
            {{ audio.work.title }}
        </a>
    </p>
{% endif %}

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

Если окажетесь в подобной ситуации — подумайте, может, стоит поступить прагматичней.

10. Не пишите код для скачивания файлов на PHP

Настал долгожданный, день когда новая версия проекта «Предание.ру» была запущена, начали приходить пользователи. Но тут нас ожидало самое большое разочарование. Начинались дни, которые я запомню надолго :)

Обычно за день сайт посещало около 14 000 посетителей. Он не выдерживал такой нагрузки. 150-200 человек в реальном времени просто ложили веб-сервер, и сайт становился недоступным.

Вы не поверите, но на протяжении многих дней нам приходилось запускать командуsudo service httpd restart каждые полчаса или час (я уже сейчас не вспомню, что нам мешало тогда использовать Cron). Пути назад уже не было. Сайт был в продакшене. Мы просто дергали апач и пытались выяснить, почему httpd процессы съедают всю память на вполне нормальной железяке (16GB, Intel® Xeon® 3.10GHz, 4CPUs).

Подобные разговоры происходили почти каждый день:

Каких только предположений и догадок у нас не было:
— «Наверное, это точно MySQL, давайте логировать медленные запросы!»
— «Смотри! У нас тут есть древовидное меню, которое использует рекурсию. Может, тут собака зарыта?»
— «А вот на главной странице отправляются 3 одновременных Ajax-запроса, может, из-за этого оверхед?»
— «Наверное, просто Doctrine слишком медленная и жрет много памяти, давай попробуем всё закешировать!»
— «Слушай, я почти уверен, что проблема из-за страницы произведения! Тут иногда создается по тысяче PHP-объектов и вообще нет пагинации!»
— «...»

Когда уже всем было понятно, что нужно попросить совета у более опытных людей, я всё не мог сдаться и просил дать нам еще несколько дней на решение проблемы.
Парни, которые переписывали соседний проект, согласились нам помочь и пригласили хорошего админа глянуть на наш сервер.

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

Тут мы решили, что вся проблема — в канале, и нам просто его не хватает. И так как мы уже тогда собирались перенести наши 3TB в облако, то решили, что после этого проблема должна уйти сама собой. Стало немного легче. Но, к нашему удивлению, сервер продолжал падать.

Как выяснялось позже, проблема была не столько в канале, сколько в тех, кто занимает пространство между ноутбуком и креслом!

We did it!

В один прекрасный день кто-то в переписке, в общем чате Skype, спросил: «А вы что-нибудь отдаете через код? Может, вы отдаете файлы для скачивания через приложение?»

Конечно! Мы отдавали файлы на скачивание через PHP. Почему? Нам нужно было, чтобы, нажимая на ссылку «скачать», пользователь получал файл, а не проигрывал его в браузере. Именно для этого мы использовали примерно такой код в контроллере:

header('Content-Type: ' . $mimeType);
header('Content-Disposition: attachment; filename=' . basename($path));
// ...
@ob_clean();
flush();
readfile($path);

Трудно поверить, но именно это сжирало всю память! Именно из-за этого много дней подряд у нас был довольно напряженный график и постоянные падения веб-сервера.

Отправляемый заголовок Content-Disposition: attachment; — обычно самый типичный способ сказать браузеру, что файл нужно скачать, а не выполнить. Но когда таким образом отдаются файлы большого размера, да еще и на нагруженном проекте — память на сервере заканчивается быстро.
Мы удалили этот несчастный код и сделали так, чтобы nginx перехватывал запросы, которые содержат подстроку /download, и посылал заголовок Content-Disposition: attachment;, чтобы браузер нормально скачивал файл.

Проблема была решена.

Вместо вывода

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

Может, вы уже давно пишете на Symfony, а может, только выбираете фреймворк (кстати, недавно я писал на тему выбора фреймворка: Laravel vs Symfony) и нашли эту статью. В любом случае, думаю, наш опыт вам пригодится.

LinkedIn

55 комментариев

Подписаться на комментарииОтписаться от комментариев Комментарии могут оставлять только пользователи с подтвержденными аккаунтами.

Перевод документации — symfony.com.ua
Если хотите тоже помочь, пишите

Вроде бы еще никто не писал. Для таких случаев, как путь к произведению, не нужны костыли, вроде автора «Без автора». Слава богу, у ТС есть интуиция, и он отказался.
Путь высчитывается и сохраняется в саму сущность или (лучше) в дополнительную таблицу со связью 1 к 1 в БД один раз при сохранении произведения.
При изменении автора или произведения он обновляется. Хотите средствами PHP. Хотите триггером в MySQL. Не принципиально. И никаких ифов в шаблонах. Поменялась структура URL? Запустили реиндекс и забыли.

Вообще, если структура данных в Mysql заставляет вас делать лишние запросы или джоины, подумайте об индексации и денормализации.

спасибо за статью!

А мы сейчас ищем докладчиков на следующий Symfony Café Kyiv, который состоится 21го мая в Киеве. Есть кто желающий? Пишите на symfonycafekyiv@gmail.com :)

А где будет проходить?

Планируется в Ciklum SkyPoint на Амосова 12 :) Там же где и в прошлый раз.

Кстати, если вы считаете, что в статье детские ошибки, которые вы уже переросли, и ищете работу — я нанимаю Симфони девов в Харькове и Берлине.

Ребята, спасибо за интересную статью, очень хорошо что вы научились, хоть и на собственных ошибках, но когда я её читал мне было БОЛЬНО.
Уже давно убеждён что Symfony это лучшее что когда-либо было написано на PHP. Это очень специфичный язык самая большая проблема в котором, как я убеждён, это низкий порог входа. И это делает очень важным такую простую истину как RTFM по симфе, по доктрине, по композеру, да вообще по всему, что вы пользуете.
Теперь детальнее:

Многие разработчики почему то считают, что файл composer.lock лучше заигнорить. Мы не были исключением и сделали это
Это от тотального непонимания того как устрояна симфа, что такое внешние пакеты, версии, и прочее. Если нет такого понимания — идём и смотрим что говорит мануал. Например сюда. А говорит он нам
all the information needed to download these files is saved in composer.json and composer.lock (which are stored in the repository), any other developer can use the project, run composer install, and download the exact same set of vendor libraries
Когда я читал эту часть статьи как раз вспомнил как меня поднимали на смех когда я сказал что джуну который тягает пакеты композером вместо того чтоб писать велосипеды стоит отдать предпочтение. Ведь в т.ч. об этом понимании как работать с менеджером пакетов я говорил. Но я даже подумать не мог что лид может делать такие ошибки.
решили, что будет лучше завести для этого новый бандл src/Company/AdminBundle
Опять — если есть колебание то на лицо не понимание что такое есть бандл в симфе. Есть непонимание — идём в мануал и смотрим что сказано там:
Bundle is a directory containing a set of files (PHP files, stylesheets, JavaScripts, images, ...) that implement a single feature
Что у вас является фичёй — админка, или возможность писать статьи, комментировать их, регистрироватся? Что приносит value в проект? Админка, как по мне, всегда была только тулзом для редактирования, не больше. Единственное что иногда она может экономить ресурс, но не приносить. Фичи же, обычно, привносят ту или иную метрику. Например кол-во регистраций, комментов — конверсионные метрики (привносят контент).
Касательно бандлов, да и вообще организации кода, ещё советую эту статью.
Немного позже, когда мне всё э то сильно надоело, я убрал эти странные сущности и просто добавил четыре булевых свойства audioType, videoType, bookType, musicType
Что вы будете делать когда появится 5й, 10й, 20й тип — каждому по потребности столбцу? Есть такой подход — называется битовая маска. Считаю это гораздо лучшим способом решения задачи чем даже хранение разных строк в одном поле — при более-менне большом количестве заметна разница в производительности, потреблении памяти, etc. Минусы можно наблюдать когда кто-то правит руками базу, но в таком случае бейте его по рукам линейкой — он waste’ит время всей команды
Часто бывало, что мы писали какой-то код, деплоили его на сервер, и всё работало. Но когда по какой-то причине приходилось разворачивать проект с нуля, внезапно начинали появляться какие то ошибки или эксепшены — и мы получали нерабочее приложение.
Чаще всего проблема возникала от того, что мы добавляли в базу данных какие-то служебные записи, но забывали добавить их в фикстуры или в скрипт установки проекта.
только что писал — это растрата времени всей команды и головная боль на пустом месте. Лучшее решение что я видел — это когда никто кроме особого админа не имеет доступ к серверам, к базе. Всё только через CI.
Как минимум — Behat, а как максимум — PHPUnit
Ребята, о чём вы? Вы работаете с деньгами, у вас бывает 14k пользователей. PHPUnit тесты и покрытие минимум 85% впринципе должны быть Acceptance criteria на код ревью. Кстати, у вас есть код ревью?
Jenkins скачивает код последнего коммита и запускает скрипт bin/jenkins_scenario, который:
— С помощью Composer устанавливает все зависимости для проекта;
— Создает базу данных;
— Запускает миграции;
— Загружает в базу данных фикстуры;
— Запускает тесты.
— собирает deb или rpm пакет и заливает его в репу пакета по версии. Затем при деплое всё развернётся нативными средствами системы.
Самый крутой сценарий что я видел — это когда CI поднимал амазоновский инстанс с нуля, разворачивал там всё, накатывал а потом после тестов дропал. Иногда это оверхед, но тем ребятам нужно было быть уверенными во всём.
Не игнорируйте JavaScript тесты
А это Acceptance criteria на ревью фронтендщиков. Кстати, а у вас вообще процессы на проекте поставлены?
Мы решили для таких целей использовать контроллер с торчащим наружу URI типа "/move/authors"
На лицо не понимание паттерна MVC. Как и раньше: есть непонимание/сомнение/etc. — RTFM
your controllers should be thin and contain nothing more than a few lines of glue-code
Контроллер взаимодействует с моделью и отдаёт данные в представление. Всё. Точка. Больше ничего контроллер не делает. Всё что больше — это FSUC который симфа не приемлет.
BTW: should описан в RFC на которыйй ссылается PSR-1 на который ссылается симфа. Вкратце — у вас должны быть очень серьёзные основания (например отсутствие возможности сделать так) для того чтобы сделать по-другому
Ещё один интересный момент, который я заметил: PHP-процесс, запущенный таким образом, работает стабильнее, чем вызов URI сайта через браузер
"стабильнее" здесь не уместное слово. Вообще даже не хочу думать как визжал и пищал ваш FPM (или тогда ещё httpd что ещё хуже), какие же лимиты в коннекшнах там да и вообще как потом эти данные попали в продакшн? Или вы в продакшне запускали этот роут? Вы серьёзно? Целая команда?
Лучшее решение — использовать для таких целей компонент Symfony Command
К сожалению — нет. Консоль симфы очень хороша, но как и всё в мире PHP — это достаточно тонкий инструмент. Нужно уметь с ним обращатся и знать его лимитейшны. Начать можно отсюда, но лучше приступить к изучению модели памяти PHP.
Но именно для вашего кейса с авторами я бы предложил Batch processing нативными средствами хранилища, или чем-то что умеет толково освобождать и реюзать память.
мы каждый раз заходили на сервер и запускали все эти команды вручную. Самое интересное — забыть одну из них, и после долго выяснять, почему ничего не работает!
Ребята, серьёзно? Это же рутина которая отлично поддаётся автоматизации. Ну вот не верю что вы за такой длительный период не слышали о деплой тулзах. Ведь уже много лет (семь так точно) используется тот же Ant (пользовал за неимением альтернатив лет семь назад). Разве не понятно что эту часть нужно делать ещё до первого деплоя? Capifony — отличный выбор, с чем вас и поздравляю. Но вы же программисты, да не дев-опсы, но ведь столько разных статей на эту тему написано и встречается везде. Вобщем это ещё раз вопрос к процессам разработки.
Один Автор может иметь много Произведений, при этом Произведение может не иметь Автора.
Первое что подумал — завёл бы автора «нет автора» с каким-нибуть резервированым id. Этот подход называется Stub. Готов похоливарить о доцельности его использования впринципе. Скажу от себя — для меня это просто достаточно удобно и применимо не только в кейсе базы данных. Простейший пример — вернуть пустую коллекцию в случае отсутствия элементов. Тоже стаб. Оверхед по памяти сравнительно мал, но экономит кучу времени и усилий по сравнению, например с возвратом null.
Не пишите код для скачивания файлов на PHP
Думал что этот пункт о мелких консольных воркерах запускаемых, например, реббитом и скачивающим ресурс с удалённого сервера по урлу в параметрах воркера. Думал — почему нет? Оказалось всё сложнее.. Итак:
sudo service httpd
Убил. Просто убил... Ребята, 14k пользователей, работа с деньгами, да зачем он вам вообще понадобился? Вы знаете как он работает? Вы знаете что он грузит на каждом реквесте? Вы знаете сколько ресурсов он потребляет? Единственное зачем его когда-либо стоило использовать это за небольшие плюшки если пых встраивать как so’шку. Но когда FPM доступен из коробки — зачем?
Каких только предположений и догадок у нас не было
Да это же основа основ — когда возникает предположение о каком-то боттлнэке первое что нужно сделать — это профайлинг. У симфы есть замечательный свой инструмент для профайлинга. Так же есть xhprof который мне нравится ещё больше. Если эти инструменты определили боттлнек в базе (мускуле) — лезем в slow_log и пользуем EXPLAIN. Гадать нечего — теряете время. Разумеется ни в коем случае не в продакшне. Продакшн вообще не должен этого всего иметь. Для этого есть стресс и лоад тестирование. Вот вкратце и совсем хрестоматийно. Ab и siege вообще полезно на ночь оставлять — утром приходишь, а тебе отчёт. Красота!..
Да, и что и где искать и профайлить вам должен был сказать access.log
я всё не мог сдаться и просил дать нам еще несколько дней на решение проблемы
Опять как? Программирование — это командный спорт. Это ведь должно было быть понятно ещё со школы/универа. Крайне рекомендую прочесть вот эту книгу. И помнить что кастомер платит не за код, а за решение его проблемы. Не можешь найти решение сам — поговори с командой, хоть ты и супер-пупер синьёр тим/тех мега лид, ты — человек и не можешь знать всё. Команда не нашла ответ — не теряй время, обратись к экспертам/более опытным товарищам. Я, если честно, вообще не понимаю как кастомер зааппрувил эти «несколько дней»...
наш код вообще почти не потреблял память, а более всего «стоял» в ожидании жесткий диск
Я уж подумал сессии не вынесли из фс, или логи такие жёсткие, или OPcache не включён. Мало ли, после всего то описаного. Итак помним: FS, даже когда у вас SSD (а при 14k пользователей иначе и быть не должно), является одним из самых частых боттлнеков. Потому:
  • сессии — в мемкеше/редисе/etc.
  • логи — все исключительно на лог. сервере
  • OPcache на проде — всегда
Мы отдавали файлы на скачивание через PHP
Вспоминаем как работает FPM, становится совсем не «трудно поверить». Единственный более-менее оправданый раз когда я такое видел — это когда нужно было трекать открывание писем на не особо нагруженом проекте. Затем я переписал всё на ребит, который разгребал access.log а статика, как и должна, отдавалась Nginx’ом. Вспоминаем ещё одно правило — при константном количестве юзер стори чем реже будет вызыватся скрипт тем выше надёжность проекта. Кстати, при ваших 14k уже давно стоит задуматся о том чтоб даже странички отдавать из fastcgi cache. За статику из аппликейшна — вообще молчу. BTW статика весьма неплохо хранится на амазоне и отдаётся оттуда.

Вместо заключения:
Ребята, спасибо что написали статью и очень хорошо, что вы научились хоть и на своих ошибках, хоть и очень дорого. На DOU очень любят ругать и недооценивать менеджеров (не вы конкретно, но такое впечатление сложилось) так вот именно Engineering Manager в большей части ответственен за то, чтоб на такие грабли девы впринципе не наступали. Именно он ответственнен за постановку процессов, набор команды под проект с соответствующими скилами и выбор лида.
Лид, за которым всегда остаётся последнее слово, должен обладать багажом знаний, пофиг — теоретических, или практических (для мменя working experience — ничто). А главное должен уметь работать с людьми, просить помощи.
То, что я увидел в статье — это уровень максимум мидла, а не лида. Ребята, давайте делать проекты правильно — ведь столько об этом уже написано...

P.S. Очень печально что люди, которых я менторил, обладали всеми этими знаниями, но их даже резюме не рассматривали, на собеседования не приглашали...

Александр, спасибо, что потратили свое время, чтобы написать такой развернутый ответ! Ваш комментарий — это почти статья)
Также спасибо за хорошую подборку ссылок — многие вещи надо будет глянуть.

Ребята, о чём вы? Вы работаете с деньгами, у вас бывает 14k пользователей. PHPUnit тесты и покрытие минимум 85% впринципе должны быть Acceptance criteria на код ревью.

Ох как это далеко от реальности молодых развивающихся проектов...

И это единственно верный подход для молодых (читай, не зарабатывающих) проектов, если, конечно, мы не говорим о работе с деньгами. Потому что тратить деньги на тестирование проекта, который не известно, выстрелит ли (да и вообще, зарелизится ли) — нерационально.

«Работа с деньгами» тоже на деле может быть очень разной и вполне оказаться «багоустойчивой» на ранних фазах.

Александр, присоединяюсь к благодарности, действительно объемный анализ.

На лицо не понимание паттерна MVC
Его использование в вебе притянуто за уши.

а чуть подробнее можете?
мне думается, что веб бекенд как раз идеально ложится на MVC:
URL запроса однозначно определяет контроллер
View определяет структуру ответа
что не так?

спасибо.
вижу, как Resource-Method-Representation совместимо с Restful.
а если не полностью согласно REST?
например, запрос «пометить все топики прочитанными» — как сервисные URL накладываются на концепцию «Resource»?

еще ссылка github.com/...adr/blob/master/README.md
The term MVC has experienced some semantic diffusion from its original meaning, especially in a web context. To resolve this diffusion, the Action-Domain-Responder pattern description is offered as a web-specific refinement of the MVC user interface pattern.

Окей. то есть, взяли концепцию MVC и уточнили-уточнили обязанности, так?
Ну, да, звучит понятнее, одназначнее. Или там какое-то серьезное различие в подходах?
PS погуглить — уже погуглил. но, думаю, чего-то недопонял, раз мне кажется всего лишь «уточненной версией MVC»

«Концепция MVC» — слишком размытое понятие. утрировано, ею можно обозвать все что:
работает c IO — как-то реагирует на входящие данные, и преобразует их в исходящие — предоставляет их отображение, с возможностью получения команд влияющих на преобразование данных, и работу IO

вообще с «паттернами ООП» такая беда: при желании их можно "прилепить«к чему угодно, и формально доказать что не стоило бы в данном случае «видеть» паттерн Х — вряд ли возможно.
И только схожий уровень «здравого смысла» может привести к взаимопониманию, а не холивару

Мое же ИМХО, MVC хорош там где и родился — для разработки GUI

В вебе о нем можно говорить только когда речь о UI. Ну там в сенче, ангуляре, и т.п.

Если же о приложении в целом, то взаимодействие сервера и браузера слишком «разорвано», чтобы применять к нему «концепцию MVC».
Собственно очень быстро народ и начал плодить MVP, MVVC, MVVM, HMVC, PAC... потому что MVC оказался как-то очень общий, недостаточный, и сама «среда вебприложения» как-то совсем непохожа на «аквариум объектов в VM Smalltalk»

Все же паттерны призваны упростить общение программистов, а если фраза «а у нас тут MVC» не дает другому программисту возможности быстро схватить дизайн приложения, то либо у них там вовсе не MVC, либо с самой концепцией MVC что-то не так.

P.S.
Кстати, даже в GUI он не всегда хорош.
Тот же GUI Swing не MVC, а Model-Delegate, где Delegate — срощенный VC. Опять же, потому что объекты в джаве весьма другие чем в смоллтоке, и сама «среда их обитания» другая

Кстати, даже в GUI он не всегда хорош.
а по-моему, как концепция, он к GUI как раз так себе применим. Так как внешние события инициируют целый водопад действий.
Собственно очень быстро народ и начал плодить MVP, MVVC, MVVM, HMVC, PAC
то есть, применительно к бекенду эти подходы тоже имеют смысл? как-то плохо мне представляется, как можно MVP натянуть на «получил запрос-сформировал ответ-уснул до следующего раза»
Так как внешние события инициируют целый водопад действий.
зависит от того что называть «внешними событиями»
то есть, применительно к бекенду эти подходы тоже имеют смысл?
зависит от того — что за бекенд.

Если бекенд делает все, а браузер работает в режиме терминала, т.е. показывает картинки, и ловит нажатия клавиш и мышки (веб изначальный), то и тут странный MVC получается. MV — есть. А контоллер — программная часть на сервере. аппаратная, то есть IO — на клиенте.

Если бекенд «чистый REST», а на клиенте SPA, то когда довебовское приложение обменивается с DB, имеющим ЯП высокого уровня — то тоже вопрос — а где в такой связке MVC?

получил запрос-сформировал ответ-уснул до следующего раза
а мне не понятно почему этот сценарий называют MVC :)
я б его вообще назвал бы ED ETL — (event driven) (extract, transform, load)
может те кто GUI не писал, и свои лисапеды — и видят что это «концепция MVC». я в упор не вижу никакого MVC.
В вебе вижу два вида приложений — терминального типа, и состоящие из двух приложений, взаимодействие между которыми происходит по протоколу высокого уровня.
и промежуточные, когда "терминал«(веб-браузер) обвешан «jQuery», и что-то кеширует, часть бизнес-логики берет на себя, и т.п.

ну и не только я. о чем и ссылки.

а увидеть что-то на что-то похожее — можно всегда и везде.

хотя... «чем похож черный ворон на письменный стол» — своего ответа у меня до сих пор нет :)

Не парьтесь, кому то просто нечем заняться. Все что дали по ссылкам выше — банальное переименовывание устоявшихся терминов при абсолютной неизменности самих принципов:
есть запрос и ресурс, есть бизнес логика (домен) и есть представление (отдаваемый респонс). И все это связывает вместе некий ресурс-хендлер. Видимо кто-то спит неспокойно, от того что кто-то однажды взял устоявшийся десктопный паттерн и просто переложил его на веб. «Всем пофиг» ))

как-то много слов. заумь какая-то.

есть более короткое описание любого паттерна:
Возьми вон ту ?уевину, срежь с нее эту 314здюлину и в?уярь вон в ту 314здопроушину.

Не можешь найти решение сам — поговори с командой, хоть ты и супер-пупер синьёр тим/тех мега лид, ты — человек и не можешь знать всё. Команда не нашла ответ — не теряй время, обратись к экспертам/более опытным товарищам.
Спасибо за эти слова! Поместил как цитату в 10 Уроков, которые Я Извлек за 10 Лет Работы Разработчиком
Но когда таким образом отдаются файлы большого размера, да еще и на нагруженном проекте — память на сервере заканчивается быстро.
Мануал, что характерно, на эту тему пишет:
readfile() will not present any memory issues, even when sending large files, on its own. If you encounter an out of memory error ensure that output buffering is off with ob_get_level().
У вас, судя по
@ob_clean();
output buffering таки on.

Только что посмотрел php.ini. У нас output_buffering = 4096 )

Но когда таким образом отдаются файлы большого размера, да еще и на нагруженном проекте — память на сервере заканчивается быстро.
XSendFile или аналоги. можно и ваш способ, но если, например, дело в проверке на права, а не в отдельных заголовках — одной настройкой nginx не обойтись

11. Не плодите бандлы сверх необходимого. Предполагается, что бандл — единица повторно (или отдельно) используемого кода, но не просто способ декомпозиции приложения.

RTFM и смотрим что сказано там:

Bundle is a directory containing a set of files (PHP files, stylesheets, JavaScripts, images, ...) that implement a single feature
О реюзабельности ничего не сказано. Значит если даже фича используется исключительно в однй части приложения, но это фича то ей место в отдельном бандле

Используйте более актуальную документацию:

When Symfony 2.0 was released, most developers naturally adopted the symfony 1.x way of dividing applications into logical modules. That’s why many Symfony apps use bundles to divide their code into logical features: UserBundle, ProductBundle, InvoiceBundle, etc.

But a bundle is meant to be something that can be reused as a stand-alone piece of software. If UserBundle cannot be used “as is” in other Symfony apps, then it shouldn’t be its own bundle. Moreover, if InvoiceBundle depends on ProductBundle, then there’s no advantage to having two separate bundles.

Best Practice
Create only one bundle called AppBundle for your application logic

Implementing a single AppBundle bundle in your projects will make your code more concise and easier to understand. Starting in Symfony 2.6, the official Symfony documentation uses the AppBundle name.

symfony.com/...creating-the-project.html

Вам не кажется что если понимать буквально «весь апп в одном бандле» то эта цитата противоречит сама себе? Здесь нужно понимать более широко. Например добавляя вэлью в проект нужно в порядке приоритета:

  • Найти готовое решение. Если оно чуть не подходит — форкнуть, если его можно доработать — пул реквесты в помощь
  • Если готоворо решения нет и эта фича не что-то очень сильно заточенного именно под ваш проект (т.е. теоретически может использоватся где-то ещё) — написать свою либу и обвязать её в свой бандл. Выложить на гитхаб/битбакет и тягать оттуда. А ещё лучше — из satis’а
  • Если это вэлью как раз именно то, что может использоватся только в этой аппе — то и место ему в апп бандле и нечего другие бандлы заводить.
Есть так же кейс использования всяких сторонних вендорских бандлов, бриджей и прочего. Зачастую именно точка этого использования повышает связанность. И этому коду тоже место именно в апп бандле. Именно потому бэст прэктис: в репозитории аппы — только один апп бандл, но это совершенно не значит что всё, что вы пишете для этой аппы болжно быть непременно в апп бандле

Так именно о том и речь — всё, что специфично для конкретного приложения, должно лежать в одном бандле. Обвязка отдельным бандлом сторонней или своей либы — вполне нормально, они не специфичны для приложения. Использование чужого бандла (пускай и форкнутого и дописанного) — тем более. Просто, «весь код аппа» подмножество «весь код написанный для аппа», код может быть написан для аппа, но не быть его частью.

Очень интересно, спасибо.

Если вы пишете на Symfony, то наверняка используете Composer. Если нет — значит, что-то у вас в самом начале не заладилось.
У Symfony2 сейчас свой инсталер, хотя у меня по какой-то причине он не сработал
Немного позже, когда мне всё э то сильно надоело, я убрал эти странные сущности и просто добавил четыре булевых свойства audioType, videoType, bookType, musicType. Жить сразу стало легче.
А по какой причине нельзя хранить тип строкой или числом в одном свойстве? Я же так понимаю, что один материал может иметь только один тип? Или несколько?
Несмотря на то, что установить Capifony в ubuntu/windows непросто, он может сильно упростить вам жизнь: для деплоя достаточно одной лишь команды, запущенной в терминале — cap deploy.
Можно, мне кажется, использовать bash-скрипт или Symfony Command

«У Symfony2 сейчас свой инсталер, хотя у меня по какой-то причине он не сработал»

Который устанавливает Composer :)

А по какой причине нельзя хранить тип строкой или числом в одном свойстве? Я же так понимаю, что один материал может иметь только один тип? Или несколько?
Один материал может иметь одновременно 4, заранее заданных типа. Строка или число не подойдут в этом случае.

Чому не створити дві Entity — Item & Type? Адже не виключено, що в майбутньому цей список може розширитися і додаватися типи, можливо, почнуть із адмінки.

Нічого такого, що відноситься до symfony не побачив. Стандартні помилки, які притаманні всім, хто мало працював з фреймворками. Власне,

Symfony2+Doctrine2 была настоящим удовольствием после Битрикса.
 це підтверджує.

А ще люди, які пишуть на symfony2, як правило, знають, що apache — зло.

Мы, кстати, в конце-концов перешли на PHP-FPM

Про те, що Apache — зло, знають усі, хто працював з ним на власному сервері (VPS/VDS, Dedicated) :)

Дякую, дуже цікава стаття

Більшість цих речей не прив’язані до Symfony.
Для Yii багато є теж актуальними.

четыре булевых свойства audioType, videoType, bookType, musicType
не очень понял почему не использовали одно поле type, определив его как кастомный DBAL тип? В базе было бы меньше нулловых полей, а удобство сохранилось бы с помощью методов isBook(), isAudio() и т.д.

Тогда в базе хранился бы JSON? Мы подумали что тогда у нас возникли бы сложности с фильтрами и привязкой к ElasticSearch.

Нет, в базе бы хранилась строка например, типа «book», «audio»... Собственно вот, что я имею ввиду: symfony.com/...okbook/doctrine/dbal.html

Хорошая статья.

мне еще понравилась на хабре
Симфония самоподдува

Отличная статья. Плюсую по очень многим пунктам.

Слушай, а давай создадим «неизвестного автора», и к нему привяжем все произведения без автора

Классика же: в массиве с названиями месяцев перед январём идёт нуллябрь :)

Плохая классика. За уши притянутый пример. Более яркий пример с календарём был бы разве что добивание всех месяцев (в т.ч. февраля) до 31 дня. Но это тоже является примером не правильного использования такого подхода как Stub.
Годный, как я считаю, пример использования стабов я описал выше (в многобуков) — это пример с коллекциями. Считаю что гораздо лучше, удобнее, менее подвержено ошибкам вернуть из метода пустую коллекцию (если элементов для неё не нашлось) чем, скажем, null. Тогда можно будет пробовать итерировать по пустой колекции вместо того чтоб:
— проверяем, коллекция ли унас, или null
— если коллекция — то итерируем
В таком случае проверку может забыть кто-то из команды (все ведь люди) кто будет пользовать этот метод.
А в комментарии, что вы привели именно о подобном к случаю с коллекцией использовании стабов и говорится

Скорее null для метода, возвращающего коллекцию, — это стаб для каких-то особых целей типа простой проверки if ($collection) вместо if(count($collection)), а то и if(count($collection->asArray())).

А так обычный классический паттерн Null Object, просто на уровне БД.

Зачем вносить идентификатор ошибки в массив тайтлов? Ведь всё равно к нему идёт прямая адресация по индексу! Ладно бы этой адресации небыло и код бы работал таким образом что единственное неверное смещение — это как раз 0. Но в том примере ведь не так — функция ведь принимает любой int. Так зачем ошибки в массиве тайтлов? А если нужно будет отдельно говорить о том, что смещение меньше, или больше допустимого? Что, ещё одну ошибку в массив тайтлов? А дальше — больше? Разве не было бы более удобно написать так:

/* возвращает название n-го месяца */
const int MIN_MONTH_INDEX = 1;
const int MAX_MONTH_INDEX = 12;
const char* ILLEGAL_MONTH_ERROR = "ILLEGAL MONTH"
char *month_name(n)
int n;
{
    static char *name[] = {
        "January",
        "February",
        "March",
        "April",
        "May",
        "Jun",
        "July",
        "August",
        "September",
        "October",
        "November",
        "December"
    };

    return (n < MIN_MONTH_INDEX || n > MAX_MONTH_INDEX)
                   ? ILLEGAL_MONTH_ERROR
                   : name[n - 1];
}
В таком случае массив имён остаётся массивом имён, обработка ошибок обработкой ошибок. По-моему тот пример, который был приведён по ссылке — тоже весьма плохой.

Кстати, давно не видел C кода... По поводу примеров из Кернигана и Ритчи — они больше для описания конкретной возможности нежели могут рассматриватся как код крупного приложения. Да и не забываем сколько времени уже прошло с написания этой замечательной книги и сколько шишек и жопыта заработало комьюнити разработчиков. Напремер книга Code Complete вышла гораздо позже и там детатьно описано почему не стоит смешивать разные типы значений

Плохая классика. За уши притянутый пример
это чтоб 1 месяц года был таки январем, а не февралем
и не надо было везде фигачить −1

Господи, ну, для чого? Для чого?!
Ну, чому не можна заюзати Carbon?
Чи треба показати, що ти не дарма в linkedin написав Middle/Senior і «тягати пакети композером» — для слабаків?

Це не конкретно до вас, якщо що.

Спасибо за интересную статью.

Подписаться на комментарии