awesomeprogrammer.com

Sharing ideas

Solving problems

Gathering solutions

Exchanging thoughts

Ruby On Rails

PHP

Postgres

Debian & Ubuntu
jQuery & CSS

Breaking Rails, Part 2, Putting Your RoR Inside Your PHP

Yo dawg, I heard you like…

Purely theoretical (obviously) case: you have existing php app and some legacy rails code that you refreshed a little bit and now someone asks you one simple question – why can’t you just put that code into that code and run it as one app? Sometimes you can reason, sometimes not. But let’s do it – I mean seriously – let’s create some kind of hybrid that will work transparently and silently for the user. Unleash your imagination and get ready for breaking every programming paradigm; treat it like a sport and get ready :).

Let’s ever get a little bit further, let’s create one rails app that will distribute its content for two different domains. Where do we start?

I assume you have some kind of working application already, in the end we want to have:

  • domain.com – our main application written in php
  • domain.com/superapp1 – rails app that is serving some part of content
  • domain.com/superapp2 – same rails app that is serving another part of content

Moreover we want to create this whole structure in a way that it can be ran as separate apps in the future just in case.

First – get familiar with mod proxy as I’ll be using Apache here with with phusion passanger. I think the most reasonable approach is to handle two different sub-domains for serving content (plus admin namespace for backend).

So looking at our application it would look like:

  • superapp1.rails.app – for serving content for ‘app1’. This url will be not directly accessible on production.
  • superapp2.rails.app – same as above, for ‘app2’
  • rails.app/admin – handling backend with restricted access

Be aware that I can’t provide where working example as solutions may vary quite a lot depending on structure of your existing applications and need, but I will give you some ideas if you will need to (theoretically) create that kind of hybrid someday.

Configure phussion passanger for your rails app, don’t forget to set PassengerFriendlyErrorPages on and RailsEnv "development" for now.

Now it’s time to start some proxy-action in your php app, setup in your virtual host config:

1
2
3
4
5
ProxyPass /superapp1 http://superapp1.rails.app
ProxyPassReverse /superapp1 http://superapp1.rails.app

ProxyPass /superapp1 http://superapp2.rails.app
ProxyPassReverse /superapp1 http://superapp2.rails.app

In your rails routes on the other hand you have to handle those sub-domains, after reading/watching this railscasts I ended up with:

config/routes.rb
1
2
3
4
5
6
7
8
9
  ...
  constraints(Subdomain) do
    # some resources and stuff
  end
  ...
  namespace :admin do
    # and here some controllers for handling backend
  end
end

and:

app/lib/subdomain.rb
1
2
3
4
5
6
7
8
class Subdomain
  def self.matches?(request)
    %w(superapp1 superapp2).each do |subdomain|
      return true if request.env['SERVER_NAME'] =~ /#{subdomain}/
    end
    false
  end
end

After reloading apache cofiguration (service apache2 reload) loading address /superapp1 from php app should bring on your brand new ror app.

Its not especially exciting and you may noticed some serious problems here:

  • you ended up with broken urls when trying to move deeper inside your proxy-ed app – as you need to provide proper urls

  • your assets are broken because you are working really on subdomain, but that can be easily fixed by adding to your apache conf: ProxyPass /assets http://rails.app/assets

  • how can you serve diffirent part of content with same rails application depeding on those domains?

  • what about log in feature for your users?

Lets stard from fixing those urls, one way is to overload url_for method from UrlHelper. I ended up with something like that:

app/helpers/url_helper.rb
1
2
3
4
5
6
7
8
module UrlHelper

  def url_for(options = nil)
    options[:host] = "#{request.domain}/#{subdomain_from_server}" if options.kind_of?(Hash) && !subdomain_from_server.empty?
    super
  end

end
app/controllers/application_controllers.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
class ApplicationController < ActionController::Base
  include UrlHelper
  helper_method :subdomain_from_server # allow with method for views (UrlHelper)

  protected
    def subdomain_from_server
    %w(superapp1 superapp2).each do |subdomain|
      return subdomain if request.env['SERVER_NAME'] =~ /#{subdomain}/ # as it's proxied we have to get subdomain from server
    end
    ''
  end

 end

Now when you call eg. articles_path or whatever you should end up with pretty proxy-ready url (that will be broken when you run your app as-it obviously).

I decided to filter content just by using CanCan (once again thank to Ryan) and setup access depending on current sub-domain in ApplicationController (you can use can and cannot on current_ability).

What’s left is authentication – how to setup backend access, log-in curret users – and that’s whole different subject. I will point three things here:

  • you can work your way with php-memcached, dalli and cookies that are shared across your applications
  • you can share existing database by setting up additional adapter in config/database.yml (then use establish_connection in your model and build up proper relation(s) onto existing database structure)
  • in the end you probably would want to lock your domains they are accessible only from internal network(s), rewrite below should do it
app/controllers/application_controllers.rb
1
2
3
4
RewriteEngine On
RewriteCond %{HTTP_HOST} ^superapp1\.rails\.app$
RewriteCond %{REMOTE_ADDR} !^((1\.2\.3\.4)|(127\.0\.0\.1))$ # allowed address(es)
RewriteRule ^.*$ http://domain.com [R]

Well, that’s it – very generall idea how it can be done, hope you liked this little piece of dark hackery ;–).

Comments