Pull to refresh

Переписываем проект с Zend Framework на Rails

Website development *
Около пяти месяцев назад я завязал с 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 — гемы

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 с загруженным окружением рельс. Прямо в консоли мы можем выполнять любой код нашего приложения. Незаменимая вещь при отладке.

image

Заодно я пересел с 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

Заключение


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

Сила рельс не только в возможностях самого фреймворка, но и в мощной инфраструктуре, которая выстроена вокруг него. Это практически устраняет рутину и скорость разработки вырастает в разы, а поддержка проекта даже приносит удовольствие.
Tags:
Hubs:
Total votes 154: ↑143 and ↓11 +132
Views 7.6K
Comments Comments 197