Yii 1.1: YII SaaS Multi Tenant application with single database

13 followers

SaaS application structure in YII using only one database

Lots of people are asking how to solve it with YII,We think its difficult with YII. But its easy to solve . There is no database triggers needed . we can simply sove it by extending a class(say "RActiveRecord") from CActiveRecord .Then extend all our model classes from that class.

create RActiveRecord.php

create a file under protected/components named RActiveRecord.php .

Extend it from CActiveRecord .

WE need override the methods beforeSave() , defaultScope() or beforeFind() and beforeDelete() .

Add a field in all tables named "tenant"

Add a field in all tables named "tenant" . This is the unique identifier .

There is no need to add "tenant" in any model class.

The RActiveRecord.php

Please paste this code to RActiveRecord.php file

<?php
 
//::Rajith:: SaaS
class RActiveRecord extends CActiveRecord
{
 
    //saving model->tenant to all tables automatic ::Rajith::
    public function beforeSave()
    {
        $tenant = $this->getTenant();
        $this->tenant = $tenant;
        return parent::beforeSave();
    }
 
    //Find only tenant match by default ::Rajith::
    //use  defaultScope() or  beforeFind()
    //comment defaultScope(), if you using beforeFind()
    public function defaultScope()
    {
        $tenant = $this->getTenant();
        return array(
        'condition'=> "tenant=:tenant",
        'params' => array(":tenant"=>$tenant));
    }
 
 
    //Find only tenant match by default ::Rajith::
    //uncomment if you using beforeFind()
    /*public function beforeFind()
    {
        $tenant = $this->getTenant();
 
        $criteria = new CDbCriteria;
        $criteria->condition = "tenant=:tenant";
        $criteria->params = array(":tenant"=>$tenant);
 
        $this->dbCriteria->mergeWith($criteria);
        parent::beforeFind();
    }*/
 
 
 
    //before deletion check for the ownership ::Rajith::
    //not working for deleteAllByAttributes
    public function beforeDelete()
    {
 
                $tenant = $this->getTenant();
                if ($this->tenant == $tenant)
                {
                        return true;
 
                }
                else
                {
 
                        return false; // prevent actual DELETE query from being run
 
                }
    }
 
 
    //to get the unique UNIQUE identifier
    public function getTenant()
    {
    //this is the unique identifier . Use your own ideas to get a unique identifier(tenent)
 
    return 'identifier-id-name';
    }
 
}

In my application i used the host name to find the tenant

Example for getTenant()

//to get the unique UNIQUE identifier
    public function getTenant()
    {
 
                $domain = $_SERVER['HTTP_HOST'];
                $connect = mysql_connect("localhost","root","password") or die("not connecting");
                mysql_select_db("databasename") or die("no db");
                $query = mysql_query("SELECT * FROM users WHERE customdomain='$domain'");
                $numrows = mysql_num_rows($query);
                if($numrows)
                {
 
                    $results = mysql_fetch_assoc($query);
                    return $results['username'];
                }
                else
                {
                    $subdomain = implode(array_slice(array_reverse(explode('.', $_SERVER['SERVER_NAME'])),2));
                    $query = mysql_query("SELECT * FROM users WHERE username='$subdomain' AND whitelabel=1");
                    $numrows = mysql_num_rows($query);
                    if($numrows)
                    {
                    return $subdomain;
                    }
                    else
                    { return 'parent';}
                }
 
    }

WE can simply use the subdomain name or domain name as the tenant.

or

use setstate at the time of login in Useridentity

Yii::app()->user->setState('tenant', "something-unique(domain-name or sudomain)");

and use that in getTenant()

//to get the unique UNIQUE identifier
    public function getTenant()
    {
 
     return Yii::app()->user->tenant;
 
    }

Final step

change in the model class

class Model-name extends RActiveRecord
{
 
...........
 
...........

Thats it!!

Please note that the commented parts in the beforeDelete() .

if you want to use the deleteAllByAttributes() and other deletion methods except delete(), then change the CActiveRecord class . because in the CActiveRecord , the beforeDelete() method only invoked for the delete() method .

Or

Use the $this->getTenant() in the conditon, check whether 'tenant' and '$this->getTenant()' matching

i dont think this is the best way to achieve SaaS in YII. Appreciate suggestions and more ideas .

Thank You - Rajith

Total 5 comments

#17489 report it
Rajith R at 2014/06/22 02:18am
@ziii

Hi,

I am using this with yii user module. The user table is also having the tenant field.

We are using this identifier saved with user as the tenant value for the active login.

#17487 report it
ziii at 2014/06/21 09:00am
managing user in SAaS Application

how do you manage user in this approach?

#13742 report it
Rajith R at 2013/06/21 03:35am
thanks Thanh

Thank you Thanh,

That is a good method

#13741 report it
Rajith R at 2013/06/21 03:33am
try using beforeFind()

if you have relations use beforeFind(), i think it will work

public function beforeFind()
    {
        $tenant = $this->getTenant();
 
        $criteria = new CDbCriteria;
        $criteria->condition = "tenant=:tenant";
        $criteria->params = array(":tenant"=>$tenant);
 
        $this->dbCriteria->mergeWith($criteria);
        parent::beforeFind();
    }

i dont think that this wiki is not a good solution for all type of uses. need good suggestions.

#13735 report it
thanhh83 at 2013/06/20 11:51am
Column 'tenant' in where clause is ambiguous

Hello,

I got error:

Integrity constraint violation: 1052 Column 'tenant' in where clause is ambiguous

When many model use this with relations.

I updated defaultScope() then it work:

public function defaultScope()
    {
        $tenant = $this->getTenant();
        return array(
        'alias' => $this->tableName(),
        'condition'=> sprintf("%s.tenant=:tenant", $this->tableName()),
        'params' => array(":tenant"=>$tenant));
    }

Thanks with great tutorial. I learn a lot from this.

Thanh

Leave a comment

Please to leave your comment.

Write new article