Andrew Havens Ruby Developer

How to use Rails in a PHP application

30 August 2012

Have you ever wanted to slowly transition a PHP application to use Ruby on Rails? You can do this by running Rails and PHP in the same application! It may sound crazy, but when writing PHP code starts driving you crazy, you start to do crazy things. In this tutorial, I will show you what you need to do to be successful.

For demonstration purposes, we'll set up a "/orders" section of the PHP app that uses Rails behind the scenes. The orders section will only allow authenticated users by sharing the logged in user session from the PHP app.

Assumptions

This guide assumes you will be using:

  • Zend Framework 1.11
  • Rails 3.2.8
  • Apache
  • Passenger Apache module for serving your Rails app

Directory structure

Let's assume you have a directory which contains your PHP app and Rails app in a single project directory:

project
  |-- php-app
      |-- application
      |-- public
      |-- ...etc
  |-- rails-app
      |-- app
      |-- public
      |-- ...etc

Configuring Apache

First things first: you need to configure Apache to serve your Rails application within your Zend Framework project:

<VirtualHost *:80>
  ServerName your.app.com
  DocumentRoot /path/to/php-app/public

  <Directory /path/to/php-app/public/orders>
    Options -MultiViews
    RailsBaseURI /
  </Directory>
</VirtualHost>

As you can see by the Apache directory configuration, Apache will be looking for a directory called orders in our PHP app's public directory. We'll need to create a symlink to our Rails app's public directory.

$ cd php-app/public
$ ln -s ../../rails-app/public orders

More Passenger configuration options can be found in the Passenger docs under deploying to a sub-uri.

Alternatively, if you wanted to use something other than Passenger, you could use Apache's modproxy. Setting up modproxy is outside the scope of this tutorial, but here's the configuration:

<VirtualHost *:80>
  ServerName your.app.com
  DocumentRoot /path/to/php-app/public
  ProxyPass /orders http://127.0.0.1:3000/orders
</VirtualHost>

Now if we were to add some scaffolding for orders and restart our Rails app, everything should work:

$ cd rails-app
$ rails g scaffold Order food:text drink:string
$ touch tmp/restart.txt
$ open http://your.app.com/orders

Sharing Sessions

In order for a user to stay logged in when navigating to our new /orders route, we will need to share sessions across both applications. We can do this by storing the session information in the database. However, it's not that simple. Rails and Zend Framework each have their own way of storing session data.

Session IDs stored in cookies

We will need to configure both Rails and Zend Framework so they store the session using the same approach.

Configuring Rails

To start storing session information in the database in the Rails application, do the following:

Enable the following line in config/initializers/session_store.rb:

YourApp::Application.config.session_store :active_record_store

Next you will generate a migration to create the database table to store the session information:

$ rails generate session_migration

Configuring PHP

Now that Rails is configured to store sessions in the database, we need to configure our PHP application to do the same. We can configure Zend Framework to store session data in the database. Add these lines to your config.ini file:

resources.session.saveHandler.class = "Zend_Session_SaveHandler_DbTable"
resources.session.saveHandler.options.name = "session"
resources.session.saveHandler.options.primary[] = "session_id"
resources.session.saveHandler.options.primary[] = "save_path"
resources.session.saveHandler.options.primary[] = "name"
resources.session.saveHandler.options.primaryAssignment[] = "sessionId"
resources.session.saveHandler.options.primaryAssignment[] = "sessionSavePath"
resources.session.saveHandler.options.primaryAssignment[] = "sessionName"
resources.session.saveHandler.options.modifiedColumn = "modified"
resources.session.saveHandler.options.dataColumn = "session_data"
resources.session.saveHandler.options.lifetimeColumn = "lifetime"

...and create the database table...

CREATE TABLE `session` (
    `session_id` char(32) NOT NULL,
    `save_path` varchar(32) NOT NULL,
    `name` varchar(32) NOT NULL DEFAULT '',
    `modified` int,
    `lifetime` int,
    `session_data` text,
    PRIMARY KEY (`Session_ID`, `save_path`, `name`)
);

Zend Framework will now automatically save session information to the database as a serialized array. Let's say you were saving the user_id in the session like this:

$session = new Zend_Session_Namespace('Auth');
$session->logged_in_user_id = 47;

If you were to view a session record in the database it would look like this:

+----------------------------------+---------------+-----------+------------+----------+-------------------------------------------+
| session_id                       | save_path     | name      | modified   | lifetime | session_data                              |
+----------------------------------+---------------+-----------+------------+----------+-------------------------------------------+
| 729d850de359a98b25a4459362c7bbcc | /var/lib/php5 | PHPSESSID | 1346463131 |     1440 | Auth|a:1:{s:17:"logged_in_user_id";i:47;} |
+----------------------------------+---------------+-----------+------------+----------+-------------------------------------------+

TODO: Sorry, still working through this part. Any tips, feel free to leave them in the comments.

Legacy Database Naming Scheme

What do you do if you used camel case instead of snake case for naming your database tables and fields and want to access those through your Rails app? Rather than update all of your PHP models to use snake case, it will be easier to tell active record to use your custom naming scheme.

When you use the Rails generators, you can specify the name of your columns as camel case:

$ rails g scaffold Post Title Body:text

...however, the table names and primary key will still be snake case. You will need to manually adjust these in the migration and model.

class Post < ActiveRecord::Base
  self.primary_key = 'ID'
  self.table_name = 'Posts'
end

But it will be cleaner in the long run if you just define a base class to handle this:

class Post < ActiveRecord::MyBase
end

module ActiveRecord
  class MyBase < ActiveRecord::Base

    self.primary_key = 'ID'

    def self.inherited(child)
      self.table_name = child.to_s.pluralize #class name is already camel cased
      super
    end

  end
end

Hopefully I've covered everything you need to know in order for this to work. If you find that I've left anything out, or have a criticism of this approach, please leave a comment so you can help others who might have stumbled across this post.