Ruby on Rails v PHP

Having come across lots of lively discussion about the pros and cons of Ruby on Rails and PHP Frameworks, I thought I’d post my tuppence-worth!

Not to be confused with the Ruby language it’s in, Ruby on Rails is an equally popular web framework. So holding a Ruby on Rails vs PHP contest may seem like comparing chalk and cheese.

My personal opinion, as a developer is that I prefer Ruby on Rails. I have used PHP for over 15 years and Ruby on Rails for over 7 years. And I do almost all my new development in Ruby on Rails. There are several reasons for this:

  • Rails is really the only “go to” framework for Ruby so I don’t have to choose between a multitude of competing frameworks.
  • Rails was a pioneer in the progression of MVC architecture and it does it very well.
  • PHP seems like old technology now, consequently there is a lot of old, bad PHP code out there – including some of mine from years ago, oh dear, did I really write that?!
  • Ruby really is a step up to a higher lever in terms of coding, away from C-like syntax to more intuitive, readable code.
  • When I switched to Ruby on Rails I found I had more time to focus on making a usable application rather than worrying about lower level functions because Rails provided so much out of the box.

PHP is still very widely used (e.g. this site uses WordPress of course) and there are a few areas where it beats Ruby on Rails:

  • Speed is the most obvious difference. PHP is faster. However, when you’re using a framework (e.g. like Rails on Ruby and Laravel on PHP) the difference isn’t so noticeable.
  • PHP has a vast choice of frameworks and this may be exactly what you want.
  • PHP is more popular (source RedMonk).
  • PHP has come on leaps and bounds since I switched to Ruby on Rails 7 years ago. Laravel is an excellent, popular framework, and PHP now has its own dependency manager called Composer.

So the choice is yours. As with all decisions, do your research.

Rails 5 + Puma + Nginx + ActionCable

In development mode things seemed to take care of themselves. Production mode was a different matter. Let’s just say it was a sensitive and delicate configuration. So in documenting this I’ve tried to condense things to the bare minimum and using the simplest configuration: hosting the “cable” within the host application (i.e. in the same virtual host). For full ActionCable documentation please visit: Rails Guides

Make sure you have a cable.js file (app/assets/javascripts/cable.js)

The “rails new” command should have created this for you.

Create a handler (app/assets/javascripts/channels/chat_channel.coffee)

App.chat = App.cable.subscriptions.create "ChatChannel",
  connected: ->
    alert 'connected'

  disconnected: ->
    alert 'disconnected'

  received: (data) ->
    # do you screen update stuff here

I’ve included popup alerts. These are quite useful when you’re doing this for the first time so you know your basic connection is working.

Make sure you have your channel and connection ActionCable files

Check for app/channels/application_cable/channel.rb and app/channels/application_cable/connection.rb. These should have been created for you. If you’re running ActionCable within your application (rather than on a separate server/vhost) you can add authentication for the connection thus:

module ApplicationCable
  class Connection < ActionCable::Connection::Base
    identified_by :current_user

    def connect
      self.current_user = find_verified_user
    end

    protected
    def find_verified_user
      if current_user = User.find_by(id: cookies.signed[:user_id])
        current_user
      else
        reject_unauthorized_connection
      end
    end
  end
end

Create your own channel file (app/channels/chat_channel.rb)

Here’s where you subscribe to your own broadcast.

class ChatChannel < ApplicationCable::Channel
  def subscribed
    stream_from 'chat_channel'
  end

  def unsubscribed
    # Any cleanup needed when channel is unsubscribed
  end
end

Set your configuration parameters

There are 2 files to change. First we must update config/environments/production.rb and set the config.action_cable constants:

config.action_cable.url = 'ws://myapp.spannersoftware.com/cable' # ws:// is non-secure, wss:// is secure
config.action_cable.allowed_request_origins = [ 'http://myapp.spannersoftware.com' ]

Important: do not add a trailing slash to either of these!

Next update config/cable.yml and set the host and port for the redis server:

development:
  adapter: async

test:
  adapter: async

production:
  adapter: redis
  url: redis://localhost:6379/1

You may not need to change this.

Add the ActionCable metatag

In app/views/layouts/application.html.erb add the following line in the <head> section:

<%= action_cable_meta_tag %>

Install and fire up the Redis server

Redis is a cool bit of kit for providing efficient messaging, for more information go here: Redis.io.

apt-get install redis-server
service redis-server start

Edit /etc/redis/redis.conf to set the port to 6379 and bind to 127.0.0.1.

Configure Nginx and Puma

Here is my Nginx virtual host example:

upstream my_app {
 server unix:/home/myapp/tmp/myapp.sock;
}
server {
 listen 10.10.10.10:80;
 server_name myapp.spannersoftware.com;

 access_log /home/myapp/log/access.log;
 error_log /home/myapp/log/error.log;
 root /home/myapp/public;

 location / {
 try_files /maint.html $uri @ruby;
 }

 location @ruby {
 proxy_pass http://my_app;
 proxy_set_header X-Real-IP $remote_addr;
 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
 proxy_set_header Host $http_host;
 proxy_redirect off;
 }

 location /cable {
 proxy_pass http://my_app;
 proxy_http_version 1.1;
 proxy_set_header Upgrade $http_upgrade;
 proxy_set_header Connection "upgrade";
 }
}

Here is my Puma config (config/puma.rb):

threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }.to_i
threads threads_count, threads_count
bind "unix:/home/myapp/tmp/bluebird2.sock"
environment ENV.fetch("RAILS_ENV") { "production" }
workers ENV.fetch("WEB_CONCURRENCY") { 2 }
daemonize true
pidfile '/home/myapp/tmp/pids/puma.pid'
I'm using a Unix socket here, the default environment is production, the process is daemonized and I've specified a pidfile location. The rest of the settings are default.

Set your app running

Assuming you have the puma gem installed correctly you should be able to run your application complete with working ActionCable.

cd /home/myapp
puma -C config/puma.rb -e production

You should see the message “connected” pop up in your browser when you first load the page. Hopefully you’ve found this helpful but please let me know if I’ve missed anything.

 

Nested Attributes Not Setting Child IDs

Developing in Rails 5 recently I was struggling to think of why my child record IDs were not being set automatically on create. I thought this happened automatically. Then I discovered that setting inverse_of in the model is vital for accessing parent models from the child without relying on a pre-existing physical record (e.g. when you are creating new records). It turns out it’s been like this since Rails 2. How blind was I!

Here’s how it works:

class Publisher < ApplicationRecord
  has_many :articles, index_errors: true, dependent: :destroy, inverse_of: :publisher
  accepts_nested_attributes_for :articles, allow_destroy: true

class Article < ApplicationRecord
  belongs_to :publisher, inverse_of: :articles
  has_many :comments, index_errors: true, dependent: :destroy, inverse_of: :article
  accepts_nested_attributes_for :comments, allow_destroy: true, reject_if: proc { |item| item[:message].blank? }

class Comment < ApplicationRecord
  belongs_to :article, inverse_of: :comments

So using inverse_of allows me to access self.article from the Comment model even when the physical record has yet to be created.