Около пяти месяцев назад я завязал с zend framework и пересел на рельсы. Тогда же начал переписывать свой проект www.okinfo.ru. Сейчас он уже закончен и sloccount показал что количество строк в проекте уменьшилось с 15000 до 4000. Мои знакомые php разработчики попросили success story и в итоге родилась эта статья. В ней я опишу как оно было, а так же немного расскажу о своем переходе на ruby.
Проработав в качестве главного разработчика www.starlook.ru около двух лет, я дошел до такой точки, когда понимаешь что пора двигаться дальше. Архитектура приложения устоялась, масштабирование заложено, код в целом устраивал и вообще весь процесс разработки был четок и отлажен. По сути мы продолжали наращивать функционалность и поддерживать уже написанное. В это время для интереса я начал присматриваться к django и rails. Несмотря на то что питон как язык мне понравился, но вот django не впечатлил. А с рельсами и руби ситуация была совершенно другая. Это можно назвать любовью с первого взгляда. И буквально через месяц изучения в Мой Круг пришло письмо с предложением вакансии ruby разработчика, хотя это была случайность, т.к. отправляющий почему-то подумал что я знаю ruby. В общем я решил что это судьба, и пройдя кучу собеседований меня все таки взялирубить на новое место.
После знакомства с рельсами было очень тяжело продолжать писать проект на ZF, слишком многое приходилось делать руками и практически на каждую задачу (теги, деревья, деплой, миграции и т.д.) писать собственные решения. Учитывая что свободного времени на проект не так много, то было решено его переписать, убив сразу двух зайцев. Во-первых упростить себе развитие и поддержку, а во-вторых подтянуть знания по соответствующим технологиям.
rvm.beginrescueend.com — установка нескольких версий руби и поддержка гемсетов.
ru.wikipedia.org/wiki/RubyGems — гемы
Этих команд достаточно для того чтобы установить рельсы, поднять сервер и начать разработку. И никаких плясок с бутстрапом.
С переходом на рельсы изменились и многие привычки при разработке.
Рельсы ведут подробный лог о том что происходит в приложении во время работы. Поэтому в терминале всегда висит окно с tail -f log/development.log (http://habrastorage.org/storage/29abc841/af70206c/2794fbe5/5d1f434f.png). В этот лог выводятся запросы к базе, поиску, кешу, письма, параметры запроса и т.п.
Выполнив эту команду мы попадаем в irb с загруженным окружением рельс. Прямо в консоли мы можем выполнять любой код нашего приложения. Незаменимая вещь при отладке.
Заодно я пересел с netbeans на vim.
habrahabr.ru/blogs/webdev/38730
guides.rubyonrails.org/routing.html
Маршрутизация в рельсах если не идеальна, то по крайней мере очень близка к этому. Следуя принципам REST можно невероятно сократить объемы кода и получить красивые и логичные урлы.
Было: pastebin.com/x0bA3siH
Стало:
Выполнив в консоли rake routes увидим: pastebin.com/mPSvzkAJ
На самом деле рельсы делают больше чем просто создают маршруты из пары строк кода. Для каждого роута создается несколько методов:
Это очень малая часть из того что может роутинг. И это вам не Zend_Controller_Router_Route_Hostname.
Поскольку сайт работает уже давно, то мне потребовалось сделать перенаправление со старых урлов на новые. Для таких задач в rails3 добавлены маршруты редиректы, которые выглядят так:
В ZF присутствуют Zend_Rest_Controller и Zend_Rest_Route, но их функционал не идет ни в какое сравнение с рельсовыми, а интерфейс роута делали специально так чтобы невозможно были ничего понять:
Когда мы пытались внедрить это в старлуке, то потратили немало времени на то чтобы заставить его заработать и на этом с переводом решили завязать.
guides.rubyonrails.org/action_controller_overview.html
Было: pastebin.com/TYhYAWEQ
Стало: pastebin.com/seJzhYMq
В работе самих контроллеров принципиальных отличий не так много. В рельсах отсутствует система хелперов аналогичная ZF, и помощников действий здесь нет, но есть классы хелперы которые используются как вью хелперы.
Тут есть пара моментов которые считаются плохим подходом к разработке (везде кроме рельс) — наследование контроллеров и ApplicationController (базовый класс для всех контроллеров). В документации предлагается общую логику используемую в контроллерах переносить в этот класс. Естественно так делать необязательно.
Этот пункт заслуживает отдельного упоминания. Попробуйте что либо поискать на www.okinfo.ru.
В каждой модели, которая должна быть проиндексирована, определяется специальный блок с приятным DSL.
После этого нужно перегенирировать конфиг sphinx, и переиндексировать данные. Это можно выполнить одной командой:
Ищем так:
На выходе получается такой конфигурационный файл sphinx, что становится страшно от мысли ручного генерирования. thinking-sphinx скрывает от нас всю сложность, предоставля удобный интерфейс и кучу возможностей. Во многом благодаря простоте использования этого компонента, я перевел на полнотекстовый поиск вообще все что можно и на сайте и в админке.
guides.rubyonrails.org/association_basics.html
guides.rubyonrails.org/active_record_querying.html
Большинство функциональности завязано именно на этот компонент. В этом есть как и плюсы так и минусы. А так да, удобная orm с кучей возможностей и ни одного самостоятельно написанного запроса.
github.com/pluginaweek/state_machine
Практически все сущности всегда имеют состояние опубликовано/не опубликовано и т.п. Для этого есть целая куча компонентов упрощающих управление и переходами состояний.
ru.wikipedia.org/wiki/Rake
Автоматизация проектных задач
К счастью в руби все библиотеки и фреймворки для повторяющихся задач используют rake. Вот, например, что можно сделать в моем проекте с помощью рейка pastebin.com/MavWT7JA. Согласитесь, впечатляет. Задачи в rake пишутся на ruby. В php есть порт — pake.
Запуск:
Когда проект был на php, то использовались всякого рода самописные скрипты и то только для задач крона. Все остальное делалось руками.
github.com/capistrano/capistrano/wiki
habrahabr.ru/blogs/webdev/110021
Раньше это была целая проблема и ритуал. Сейчас же все сводится к одной строчке (хотя команд конечно же больше):
Он тебе и зависимости установит, миграции накатит, индекс перегенирирует, закроет сайт, откроет, откатится если вдруг что пойдет не так. Возможностей уйма и все настраиваемо.
gembundler.com
Для руби существует такая замечательная вещь как bundler. Этот компонент управляет зависимостями. Умеет их устанавливать, обновлять, замораживать (для продакшена) и много чего другого. Управляется через консоль. Прописанные гемы для приложения выглядят так: pastebin.com/50pe40Sb.
Деревья — Ancestry (use Materialized path)
Хлебные крошки — breadcrumbs_on_rails
Геолокация — graticule
Теги — acts-as-taggable-on
Картинки — paperclip
Внешние ключи — foreigner
Тестирование — rspec, factory_girl_rails, rcov
Деплой — capistrano, capistrano-ext, capistrano_colors
github.com/plataformatec/devise
Возможности: Omniauthable, Confirmable, Recoverable, Registerable, Rememberable, Trackable, Timeoutable, Validatable, Lockable, Encryptable, Database Authenticatable, Token Authenticatable.
Компонент вообще фантастический. Две команды в консоли и куча работающего функционала из коробки. Руками все тоже самое написать займет уйму времени и все равно так здорово не получится.
github.com/plataformatec/simple_form
Принципиальное отличие от ZF, Django, в том что форма создается в шаблоне. И как показала практика в рельсах это сделано настолько удобно что без затруднения создаются формы невероятной сложности. Этого нельзя сказать о Zend_Form (вообще о zend_form можно много нехорошего сказать).
В примере видно что форма может работать с ассоциациями.
habrahabr.ru/blogs/testing/52929
github.com/thoughtbot/factory_girl
Было: pastebin.com/3tEctqmT
Стало: pastebin.com/VmawXxbc
На прошлой работе я изобретал FactoryGirl самостоятельно, в рельсах, к счастью, не пришлось. Так же не пришлось заниматься интеграцией с приложением (тесты работают из коробки без всяких шаманств с бутстрапом). И было приятно открыть для себя что то отличное от обычных assert’ов.
Правда есть один минус по сравнению с php. Невозможно увидеть покрытие тестами вьюх, что при тестировании контроллеров очень важно.
guides.rubyonrails.org/action_mailer_basics.html
rusrails.ru/action-mailer-basics
Этот компонент в rails получился очень удачным. Отправка писем происходит из сервисов генерируемых командой rails g mailer MailerName. Работает он так же как и контроллер. На каждый метод создается шаблон с телом письма. Отправка происходит так: CompanyMailer.after_registration(@company).deliver
Еще один момент по поводу рельс. Rails environment это не просто слова, как это сделано в ZF, а действительно другое поведение компонентов: кеширование, логирование, отправка почты и многое другое работает по разному в зависимости от текущего env. Так вот письма в dev режиме (а это режим по дефолту) отправляются в лог о котором я писал выше.
И тест (кстати сгенерированный автоматически):
github.com (платный аккаунт) — для кода.
newrelic.com — профилирование.
hoptoadapp.com/pages/home — для нотификаций об ошибках. Этот сервис здорово облегчает жизнь и работет с кучей языков, в том числе php. Он агрегирует все возникающие в продакшене ошибки и высылает оповещения (например на почту), при этом на одну ошибку — одно письмо, даже если ошибка повторилась много раз, что всегда и происходит. Может на основе них делать тикеты, интегрируется с гитхабом и другими сервисами. Починив ошибку ее можно зарезолвить, так же все ошибки резолвятся после деплоя, для того чтобы заработала отсылка оповещений при повторном возникновении. Умеет показывать похожие ошибки. Сервис платный, но недорогой и по моему мнению является must have. А еще он умеет все тоже самое с js.
Кстати существует бесплатный аналог github.com/jdpace/errbit (Errbit is an open source, self-hosted error catcher. It is Hoptoad API compliant so you can just point the Hoptoad notifier at your Errbit server if you are already using Hoptoad).
И нашел вот такой клиент для php github.com/rich/php-hoptoad-notifier
Не скажу что сразу вот так все было легко просто и понятно. Мне во многом помогла моя текущая работа с рельсами. Некоторые части сайта я даже на рельсах успел переписать, но сейчас я уже доволен результатом.
Сила рельс не только в возможностях самого фреймворка, но и в мощной инфраструктуре, которая выстроена вокруг него. Это практически устраняет рутину и скорость разработки вырастает в разы, а поддержка проекта даже приносит удовольствие.
Предыстория
Проработав в качестве главного разработчика www.starlook.ru около двух лет, я дошел до такой точки, когда понимаешь что пора двигаться дальше. Архитектура приложения устоялась, масштабирование заложено, код в целом устраивал и вообще весь процесс разработки был четок и отлажен. По сути мы продолжали наращивать функционалность и поддерживать уже написанное. В это время для интереса я начал присматриваться к django и rails. Несмотря на то что питон как язык мне понравился, но вот django не впечатлил. А с рельсами и руби ситуация была совершенно другая. Это можно назвать любовью с первого взгляда. И буквально через месяц изучения в Мой Круг пришло письмо с предложением вакансии ruby разработчика, хотя это была случайность, т.к. отправляющий почему-то подумал что я знаю ruby. В общем я решил что это судьба, и пройдя кучу собеседований меня все таки взяли
Зачем?
После знакомства с рельсами было очень тяжело продолжать писать проект на ZF, слишком многое приходилось делать руками и практически на каждую задачу (теги, деревья, деплой, миграции и т.д.) писать собственные решения. Учитывая что свободного времени на проект не так много, то было решено его переписать, убив сразу двух зайцев. Во-первых упростить себе развитие и поддержку, а во-вторых подтянуть знания по соответствующим технологиям.
Настройка окружения
rvm.beginrescueend.com — установка нескольких версий руби и поддержка гемсетов.
ru.wikipedia.org/wiki/RubyGems — гемы
sudo apt-get install rubygems
gem install rails
rails s
Этих команд достаточно для того чтобы установить рельсы, поднять сервер и начать разработку. И никаких плясок с бутстрапом.
Процесс
С переходом на рельсы изменились и многие привычки при разработке.
Рельсы ведут подробный лог о том что происходит в приложении во время работы. Поэтому в терминале всегда висит окно с tail -f log/development.log (http://habrastorage.org/storage/29abc841/af70206c/2794fbe5/5d1f434f.png). В этот лог выводятся запросы к базе, поиску, кешу, письма, параметры запроса и т.п.
rails c
Выполнив эту команду мы попадаем в irb с загруженным окружением рельс. Прямо в консоли мы можем выполнять любой код нашего приложения. Незаменимая вещь при отладке.
Заодно я пересел с netbeans на vim.
Маршрутизация и REST
habrahabr.ru/blogs/webdev/38730
guides.rubyonrails.org/routing.html
Маршрутизация в рельсах если не идеальна, то по крайней мере очень близка к этому. Следуя принципам REST можно невероятно сократить объемы кода и получить красивые и логичные урлы.
Было: pastebin.com/x0bA3siH
Стало:
namespace :admin do
resources :brands do
resources :systems
end
end
Выполнив в консоли rake routes увидим: pastebin.com/mPSvzkAJ
На самом деле рельсы делают больше чем просто создают маршруты из пары строк кода. Для каждого роута создается несколько методов:
admin_brand_systems_path(@brand) # путь без указания хоста
admin_brand_systems_url(@brand) # полный путь включая хост
Это очень малая часть из того что может роутинг. И это вам не Zend_Controller_Router_Route_Hostname.
Поскольку сайт работает уже давно, то мне потребовалось сделать перенаправление со старых урлов на новые. Для таких задач в rails3 добавлены маршруты редиректы, которые выглядят так:
# Хорошая идея для реализации в ZF
match "/company/:id" => redirect("/companies/%{id}")
В ZF присутствуют Zend_Rest_Controller и Zend_Rest_Route, но их функционал не идет ни в какое сравнение с рельсовыми, а интерфейс роута делали специально так чтобы невозможно были ничего понять:
$restRoute = new Zend_Rest_Route($front, array(), array(
'api',
'backlog' => array('task'),
));
$router->addRoute('rest', $restRoute);
Когда мы пытались внедрить это в старлуке, то потратили немало времени на то чтобы заставить его заработать и на этом с переводом решили завязать.
Контроллеры
guides.rubyonrails.org/action_controller_overview.html
Было: pastebin.com/TYhYAWEQ
Стало: pastebin.com/seJzhYMq
В работе самих контроллеров принципиальных отличий не так много. В рельсах отсутствует система хелперов аналогичная ZF, и помощников действий здесь нет, но есть классы хелперы которые используются как вью хелперы.
Тут есть пара моментов которые считаются плохим подходом к разработке (везде кроме рельс) — наследование контроллеров и ApplicationController (базовый класс для всех контроллеров). В документации предлагается общую логику используемую в контроллерах переносить в этот класс. Естественно так делать необязательно.
Полнотекстовый поиск (ThinkingSphinx)
Этот пункт заслуживает отдельного упоминания. Попробуйте что либо поискать на www.okinfo.ru.
В каждой модели, которая должна быть проиндексирована, определяется специальный блок с приятным DSL.
define_index do
indexes title
indexes body
indexes sub_categories(:title)
has created_at
has taggings(:tag_id), :as => :tag_ids, :facet => true
has companies_sub_categories :sub_category_id
has addresses :city_id
has :calculator_page # :without => {:calculator_page => 0}
has :site
has :reviews_count
end
После этого нужно перегенирировать конфиг sphinx, и переиндексировать данные. Это можно выполнить одной командой:
rake ts:rebuild
Ищем так:
Company.search ‘запрос’
На выходе получается такой конфигурационный файл sphinx, что становится страшно от мысли ручного генерирования. thinking-sphinx скрывает от нас всю сложность, предоставля удобный интерфейс и кучу возможностей. Во многом благодаря простоте использования этого компонента, я перевел на полнотекстовый поиск вообще все что можно и на сайте и в админке.
ActiveRecord
guides.rubyonrails.org/association_basics.html
guides.rubyonrails.org/active_record_querying.html
class Company < ActiveRecord::Base
acts_as_taggable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable, :trackable
belongs_to :image
has_many :weekly_newsletters, :class_name => 'Notice', :conditions => { :notice_type => 'weekly_newsletter' }
has_many :addresses
has_many :sub_categories, :through => :companies_sub_categories
validates :site,
:uniqueness => true,
:format => { :with => URI::regexp(%w(http https)) },
:allow_blank => true
validates :title, :presence => true, :uniqueness => true
validates :twitter_page, :format => { :with => URI::regexp(%w(http https)) },
:allow_blank => true
scope :by_rating, order('ratings_sum DESC')
scope :with_logo, where('image_id IS NOT NULL').includes(:image)
Большинство функциональности завязано именно на этот компонент. В этом есть как и плюсы так и минусы. А так да, удобная orm с кучей возможностей и ни одного самостоятельно написанного запроса.
State Machine
github.com/pluginaweek/state_machine
Практически все сущности всегда имеют состояние опубликовано/не опубликовано и т.п. Для этого есть целая куча компонентов упрощающих управление и переходами состояний.
# ActiveRecord model
state_machine :state, :initial => :moderate do
state :off, :human_name => 'Отключена'
state :moderate, :human_name => 'На модерации'
state :published, :human_name => 'Опубликована' do
validates :uri, :presence => true
end
event :publish, :human_name => 'Опубликовать' do
transition :off => :published
transition :moderate => :published
end
event :off, :human_name => 'Отключить' do
transition :published => :off
end
end
Переодически задачи и служебные задачи (Rake, cron)
ru.wikipedia.org/wiki/Rake
Автоматизация проектных задач
К счастью в руби все библиотеки и фреймворки для повторяющихся задач используют rake. Вот, например, что можно сделать в моем проекте с помощью рейка pastebin.com/MavWT7JA. Согласитесь, впечатляет. Задачи в rake пишутся на ruby. В php есть порт — pake.
# Еженедельная рассылка писем по компаниям
namespace :app do
namespace :notice do
desc 'Weekly newsletter'
task :weekly_for_company => :environment do
Company.state.find_each do |company| # find_each использует batch
last_notice = company.weekly_newsletters.last
next if last_notice && (Time.now - last_notice.created_at < 1.week)
notice = company.weekly_newsletters.create
notice.save
CompanyMailer.weekly_newsletter(company).deliver
sleep(1)
end
end
end
end
Запуск:
rake app:notice:weekly_for_company
Когда проект был на php, то использовались всякого рода самописные скрипты и то только для задач крона. Все остальное делалось руками.
Deploy (capistrano)
github.com/capistrano/capistrano/wiki
habrahabr.ru/blogs/webdev/110021
Раньше это была целая проблема и ритуал. Сейчас же все сводится к одной строчке (хотя команд конечно же больше):
cap deploy:migrations
Он тебе и зависимости установит, миграции накатит, индекс перегенирирует, закроет сайт, откроет, откатится если вдруг что пойдет не так. Возможностей уйма и все настраиваемо.
Используемые сторонние компоненты (Bundler)
gembundler.com
Для руби существует такая замечательная вещь как bundler. Этот компонент управляет зависимостями. Умеет их устанавливать, обновлять, замораживать (для продакшена) и много чего другого. Управляется через консоль. Прописанные гемы для приложения выглядят так: pastebin.com/50pe40Sb.
Деревья — Ancestry (use Materialized path)
Хлебные крошки — breadcrumbs_on_rails
Геолокация — graticule
Теги — acts-as-taggable-on
Картинки — paperclip
Внешние ключи — foreigner
Тестирование — rspec, factory_girl_rails, rcov
Деплой — capistrano, capistrano-ext, capistrano_colors
Авторизация (Devise)
github.com/plataformatec/devise
Возможности: Omniauthable, Confirmable, Recoverable, Registerable, Rememberable, Trackable, Timeoutable, Validatable, Lockable, Encryptable, Database Authenticatable, Token Authenticatable.
Компонент вообще фантастический. Две команды в консоли и куча работающего функционала из коробки. Руками все тоже самое написать займет уйму времени и все равно так здорово не получится.
Формы (Simple_Form)
github.com/plataformatec/simple_form
Принципиальное отличие от ZF, Django, в том что форма создается в шаблоне. И как показала практика в рельсах это сделано настолько удобно что без затруднения создаются формы невероятной сложности. Этого нельзя сказать о Zend_Form (вообще о zend_form можно много нехорошего сказать).
<%= simple_form_for(resource, :url => company_registration_path) do |f| %>
<%= f.input :email %>
<%= f.input :password %>
<%= f.input :password_confirmation %>
<%= f.association :forma, :label_method => :title %>
<%= f.input :title %>
<%= f.association :sub_categories, :label_method => :title %>
<%= f.simple_fields_for :address do |address| %>
<%= address.association :city, :as => :select, :collection => cities(@company.address),
:input_html => { :id => "city_id" } %>
<%= address.input :street %>
<%= address.input :house, :required => false %>
<% end %>
<%= f.submit 'Зарегистрироваться' %>
<% end %>
В примере видно что форма может работать с ассоциациями.
Тесты (Rspec, FactoryGirl, Rcov)
habrahabr.ru/blogs/testing/52929
github.com/thoughtbot/factory_girl
Было: pastebin.com/3tEctqmT
Стало: pastebin.com/VmawXxbc
На прошлой работе я изобретал FactoryGirl самостоятельно, в рельсах, к счастью, не пришлось. Так же не пришлось заниматься интеграцией с приложением (тесты работают из коробки без всяких шаманств с бутстрапом). И было приятно открыть для себя что то отличное от обычных assert’ов.
Правда есть один минус по сравнению с php. Невозможно увидеть покрытие тестами вьюх, что при тестировании контроллеров очень важно.
Работа с почтой
guides.rubyonrails.org/action_mailer_basics.html
rusrails.ru/action-mailer-basics
Этот компонент в rails получился очень удачным. Отправка писем происходит из сервисов генерируемых командой rails g mailer MailerName. Работает он так же как и контроллер. На каждый метод создается шаблон с телом письма. Отправка происходит так: CompanyMailer.after_registration(@company).deliver
Еще один момент по поводу рельс. Rails environment это не просто слова, как это сделано в ZF, а действительно другое поведение компонентов: кеширование, логирование, отправка почты и многое другое работает по разному в зависимости от текущего env. Так вот письма в dev режиме (а это режим по дефолту) отправляются в лог о котором я писал выше.
И тест (кстати сгенерированный автоматически):
describe ColumnistMailer do
describe "after_registration" do
let(:mail) { ColumnistMailer.after_registration }
it "renders the headers" do
mail.subject.should eq("After registration")
mail.to.should eq(["to@example.org"])
mail.from.should eq(["from@example.com"])
end
it "renders the body" do
mail.body.encoded.should match("Hi")
end
end
end
Вспомогательные сервисы
github.com (платный аккаунт) — для кода.
newrelic.com — профилирование.
hoptoadapp.com/pages/home — для нотификаций об ошибках. Этот сервис здорово облегчает жизнь и работет с кучей языков, в том числе php. Он агрегирует все возникающие в продакшене ошибки и высылает оповещения (например на почту), при этом на одну ошибку — одно письмо, даже если ошибка повторилась много раз, что всегда и происходит. Может на основе них делать тикеты, интегрируется с гитхабом и другими сервисами. Починив ошибку ее можно зарезолвить, так же все ошибки резолвятся после деплоя, для того чтобы заработала отсылка оповещений при повторном возникновении. Умеет показывать похожие ошибки. Сервис платный, но недорогой и по моему мнению является must have. А еще он умеет все тоже самое с js.
Кстати существует бесплатный аналог github.com/jdpace/errbit (Errbit is an open source, self-hosted error catcher. It is Hoptoad API compliant so you can just point the Hoptoad notifier at your Errbit server if you are already using Hoptoad).
И нашел вот такой клиент для php github.com/rich/php-hoptoad-notifier
Заключение
Не скажу что сразу вот так все было легко просто и понятно. Мне во многом помогла моя текущая работа с рельсами. Некоторые части сайта я даже на рельсах успел переписать, но сейчас я уже доволен результатом.
Сила рельс не только в возможностях самого фреймворка, но и в мощной инфраструктуре, которая выстроена вокруг него. Это практически устраняет рутину и скорость разработки вырастает в разы, а поддержка проекта даже приносит удовольствие.