Yii PHP : dynamic database connections for read/write data - php

i am developing a app that scans/crawls/monitors certain websites
the app is available for free at www.linkbook.co
now, i will explain what i want to do:
I have a main DB, that stores the websites, the urls found on the websites and url patterns from each website; The website knows the Db that he has been assigned to, the url knows the website id;
Because i have a 2 GB / DB limit, i need secondary DB's.
I read all from http://www.yiiframework.com/wiki/123/multiple-database-support-in-yii/ and what Google could find, and there is not a single complete example using dinamicaly the connection;
So, each time my app scans a website, each url found will be inserted in the main DB, and also in the DB that has been assigned to the website that the url belongs to.
In time, the data from the main DB gets deleted, but it is still available in the secondary DB.
So, only the HOT info gets stored in the main but they remain saved in the secondary db's
The Yii guys give this example, but i need a parameter, from 1 to N, N=1->infinite,in order to switch to the right DB.
class MyActiveRecord extends CActiveRecord {
...
private static $dbadvert = null;
protected static function getAdvertDbConnection()
{
if (self::$dbadvert !== null)
return self::$dbadvert;
else
{
self::$dbadvert = Yii::app()->dbadvert;
if (self::$dbadvert instanceof CDbConnection)
{
self::$dbadvert->setActive(true);
return self::$dbadvert;
}
else
throw new CDbException(Yii::t('yii','Active Record requires a "db" CDbConnection application component.'));
}
}
...
$dbadvert needs to be dinamic, this is my problem.
also, the tables from the secondary DB's are not exactly the same, as i dont need all the fields from all the tables, and some tables get dropped also, so i need a model;
the model i can write it, it's not hard, i will just delete some fields;
this is what i have now, just the insert, in a specific DB, db1 a.k.a. linkbookco1
$command = Yii::app()->db1->createCommand("INSERT INTO `url` (
`id` ,
`website_id` ,
`link` ,
`name` ,
`created` ,
`instance_scanner_id` ,
`status`
)
VALUES (
NULL , '".($model_url->website_id)."', '".($model_url->link)."', '".($model_url->name)."', '".($model_url->created)."', '".($model_url->instance_scanner_id)."', '".($model_url->status)."'
);
");
$command->query();
db1 and it's params are mentioned in the config file, as the yii developers say;

from your example I can guess you have a secondary database connection configured in your app like
'dbadvert' => array(
'class' => 'CDbConnection',
...
)
the point where you need to get the database connection you have this:
self::$dbadvert = Yii::app()->dbadvert;
So, to have multiple database connections you'd need to add them to your database configuration
'dbadvert1' => array(
'class' => 'CDbConnection',
...
)
'dbadvert2' => array(
'class' => 'CDbConnection',
...
)
And in your code you can do something like
self::$dbadvert = Yii::app()->getComponent("dbadvert".Yii::app()->request->getQuery('dbid', ''));

Related

CakePHP Unit tests are ignoring Fixtures/test databases and using application's data/databases

I have a legacy application which is running on CakePHP 4.3.8. Historically this application never had any unit tests but we're starting to add them as we work on new features.
The application uses 8 different MariaDB database connections. These are configured in config/app_local.php and all have their own key. There is a corresponding "test" database for each one prefixed with test_. As an example:
dev_notification_db: Local database for a system notifications feature of the app.
test_notification_db: Test database for the above
All database names follow this convention and have appropriate credentials and connection details to a local install of MariaDB 10.x. A schema with the appropriate name has been created locally and access has been granted to the user/pass referenced in the configuration. We have not had any connection or permissions errors.
In the dev_notification_db there are around 3400 rows of data.
I'm trying to write some unit tests following CakePHP's Testing docs. In this case I want test_notification_db to contain just 3 rows of data since for a particular test I'm writing I don't need all 3400 rows from the application database and certainly don't want them in a version controlled Fixture.
I have created a Fixture in tests/Fixture/NotificationsFixture.php which contains the 3 rows I want to add to the test database, test_notification_db. This file was created using the following command:
bin/cake bake fixture --connection dev_notification_db --conditions 1=1 --count 3400 --records Notifications
When the file was created it contained all 3400 rows of data from dev_notification_db. I manually removed all but the 3 of them that were necessary for testing purposes. The reason I did it in this way is because using the command above also reads the table structure and adds it to NotificationsFixture.php, meaning the table with the same columns/data types can be created on test_notification_db.
NotificationsFixture.php looks like this
class NotificationsFixture extends TestFixture
{
public $fields = [
// This is essentially the schema of the `notifications` table
// e.g. 'id' => ['type' => 'integer', 'length' => null, 'unsigned' => true, 'null' => false, 'default' => null, 'comment' => '', 'autoIncrement' => true, 'precision' => null]
// Other columns...
];
public function init(): void
{
$this->records = [
[
'id' => 1,
'column1' => 'foo',
'column2' => 'bar',
'column3' => 'baz',
],
// Other rows of test data...
}
}
In the init() method above, $this->records contains 3 rows of data which are the ones I want to use in my fixture.
According to the Creating Fixtures section of the CakePHP docs, it says
Fixtures defines the records that will be inserted into the test database at the beginning of each test.
Therefore my understanding is that the 3 rows in $this->records should end up in the test_notification_db when I'm running my tests.
There isn't anything else in the NotificationsFixture.php file, for example something telling it to use a different Data Source from the app_local.php config file: it has $fields and the init() method, and that's all.
My test for this is in tests/TestCase/Model/Table/NotificationsTest.php. One of my tests relies on it reading the Fixture data, i.e. the 3 rows from test_notification_db. In order to do this I've added the following to my test:
protected $fixtures = [
'app.Notifications',
// ...
];
public function setUp(): void
{
parent::setUp();
$this->Notifications = TableRegistry::getTableLocator()->get('Notifications');
}
public function testGetNotifications()
{
$userNotifications = $this->Notifications->find()->where(['user_id' => 1])->count();
debug($userNotifications);
die;
}
To explain my understanding of the code above:
The $fixtures array contains app.Notifications. This is consistent to what's shown in the CakePHP docs regarding Loading Fixtures in your Test Cases. It specficially says:
After you’ve created your fixtures, you’ll want to use them in your test cases. In each test case you should load the fixtures you will need. You should load a fixture for every model that will have a query run against it.
So my understanding is I'm loading the NotificationsFixture.php file referenced earlier.
The setUp() method gives me a reference to the Model that interacts with the notifications table, i.e. src/Model/Table/NotificationsTable.php. Given that this is being run in a Test Suite - and after reviewing the docs - this should be interacting with the test database (test_notification_db) NOT the application database (dev_notification_db).
The actual test uses the model in the point above and attempts to find notifications for a given user ID. The count in this case comes back as 400. In the application database there are 400 rows for the user ID given. However, in the fixture there are only 3 rows (all of which are for the user in question). Therefore this should come back as 3, not 400.
Upon debugging the full result set it's clear that the data is being loaded from dev_notification_db and not the test database, test_notification_db.
Why is this? If the purpose of the test suite is to be able to interact with the test database and the Fixtures are - quote - "records that will be inserted into the test database at the beginning of each test" then this is certainly not doing that.
What is missing here, or making it interact with the application's database and not the separate database for testing purposes?

Yii active record relation limit to one record

I am using PHP Yii framework's Active Records to model a relation between two tables. The join involves a column and a literal, and could match 2+ rows but must be limited to only ever return 1 row.
I'm using Yii version 1.1.13, and MySQL 5.1.something.
My problem isn't the SQL, but how to configure the Yii model classes to work in all cases. I can get the classes to work sometimes (simple eager loading) but not always (never for lazy loading).
First I will describe the database. Then the goal. Then I will include examples of code I've tried and why it failed.
Sorry for the length, this is complex and examples are necessary.
The database:
TABLE sites
columns:
id INT
name VARCHAR
type VARCHAR
rows:
id name type
-- ------- -----
1 Site A foo
2 Site B bar
3 Site C bar
TABLE field_options
columns:
id INT
field VARCHAR
option_value VARCHAR
option_label VARCHAR
rows:
id field option_value option_label
-- ----------- ------------- -------------
1 sites.type foo Foo Style Site
2 sites.type bar Bar-Like Site
3 sites.type bar Bar Site
So sites has an informal a reference to field_options where:
field_options.field = 'sites.type' and
field_options.option_value = sites.type
The goal:
The goal is for sites to look up the relevant field_options.option_label to go with its type value. If there happens to be more than one matching row, pick only one (any one, doesn't matter which).
Using SQL this is easy, I can do it 2 ways:
I can join using a subquery:
SELECT
sites.id,
f1.option_label AS type_label
FROM sites
LEFT JOIN field_options AS f1 ON f1.id = (
SELECT id FROM field_options
WHERE
field_options.field = 'sites.type'
AND field_options.option_value = sites.type
LIMIT 1
)
Or I can use a subquery as a column reference in the select clause:
SELECT
sites.id,
(
SELECT id FROM field_options
WHERE
field_options.field = 'sites.type'
AND field_options.option_value = sites.type
LIMIT 1
) AS type_label
FROM sites
Either way works great. So how do I model this in Yii??
What I've tried so far:
1. Use "on" array key in relation
I can get a simple eager lookup to work with this code:
class Sites extends CActiveRecord
{
...
public function relations()
{
return array(
'type_option' => array(
self::BELONGS_TO,
'FieldOptions', // that's the class for field_options
'', // no normal foreign key
'on' => "type_option.id = (SELECT id FROM field_options WHERE field = 'sites.type' AND option_value = t.type LIMIT 1)",
),
);
}
}
This works when I load a set of Sites objects and force it to eager load type_label, e.g. Sites::model()->with('type_label')->findByPk(1).
It does not work if type_label is lazy-loaded.
$site = Sites::model()->findByPk(1);
$label = $site->type_option->option_label; // ERROR: column t.type doesn't exist
2. Force eager loading always
Building on #1 above, I tried forcing Yii to always to eager loading, never lazy loading:
class Sites extends CActiveRecord
{
public function relations()
{
....
}
public function defaultScope()
{
return array(
'with' => array( 'type_option' ),
);
}
}
Now everything always works when I load Sites, but it's no good because there are other models (not pictured here) that have relations that point to Sites, and those result in errors:
$site = Sites::model()->findByPk(1);
$label = $site->type_option->option_label; // works now
$other = OtherModel::model()->with('site_relation')->findByPk(1); // ERROR: column t.type doesn't exist, because 't' refers to OtherModel now
3. Make the reference to the base table somehow relative
If there was a way that I could refer to the base table, other than "t", that was guaranteed to point to the correct alias, that would work, e.g.
'on' => "type_option.id = (SELECT id FROM field_options WHERE field = 'sites.type' AND option_value = %%BASE_TABLE%%.type LIMIT 1)",
where %%BASE_TABLE%% always refers to the correct alias for table sites. But I know of no such token.
4. Add a true virtual database column
This way would be the best, if I could convince Yii that the table has an extra column, which should be loaded just like every other column, except the SQL is a subquery -- that would be awesome. But again, I don't see any way to mess with the column list, it's all done automatically.
So, after all that... does anyone have any ideas?
EDIT Mar 21/15: I just spent a long time investigating the possibility of subclassing parts of Yii to get the job done. No luck.
I tried creating a new type of relation based on BELONGS_TO (class CBelongsToRelation), to see if I could somehow add in context sensitivity so it could react differently depending on whether it was being lazy-loaded or not. But Yii isn't built that way. There is no place where I can hook in code during query buiding from inside a relation object. And there is also no way I can tell even what the base class is, relation objects have no link back to the parent model.
All of the code that assembles these queries for active records and their relations is locked up in a separate set of classes (CActiveFinder, CJoinQuery, etc.) that cannot be extended or replaced without replacing the entire AR system pretty much. So that's out.
I then tried to see if I can create "fake" database column entries that would actually be a subquery. Answer: no. I figured out how I could add additional columns to Yii's automatically generated schema data. But,
a) there's no way to define a column in such a way that it can be a derived value, Yii assumes it's a column name in way too many places for that; and
b) there also doesn't appear to be any way to avoid having it try to insert/update to those columns on save.
So it really is looking like Yii (1.x) just does not have any way to make this happen.
Limited solution provided by #eggyal in comments: #eggyal has a suggestion that will meet my needs. He suggests creating a MySQL view table to add extra columns for each label, using a subquery to look up the value. To allow editing, the view would have to be tied to a separate Yii class, so the downside is everywhere in my code I need to be aware of whether I'm loading a record for reading only (must use the view's class) or read/write (must use the base table's class, does not have the extra columns). That said, it is a workable solution for my particular case, maybe even the only solution -- although not an answer to this question as written, so I'm not going to put it in as an answer.
OK, after a lot of attempts, I have found a solution. Thanks to #eggyal for making me think about database views.
As a quick recap, my goal was:
link one Yii model (CActiveRecord) to another using a relation()
the table join is complex and could match more than one row
the relation must never join more than one row (i.e. LIMIT 1)
I got it to work by:
creating a view from the field_options base table, using SQL GROUP BY to eliminate duplicate rows
creating a separate Yii model (CActiveRecord class) for the view
using the new model/view for the relation(), not the original table
Even then there were some wrinkles (maybe a Yii bug?) I had to work around.
Here are all the details:
The SQL view:
CREATE VIEW field_options_distinct AS
SELECT
field,
option_value,
option_label
FROM
field_options
GROUP BY
field,
option_value
;
This view contains only the columns I care about, and only ever one row per field/option_value pair.
The Yii model class:
class FieldOptionsDistinct extends CActiveRecord
{
public function tableName()
{
return 'field_options_distinct'; // the view
}
/*
I found I needed the following to override Yii's default table data.
The view doesn't have a primary key, and that confused Yii's AR finding system
and resulted in a PHP "invalid foreach()" error.
So the code below works around it by diving into the Yii table metadata object
and manually setting the primary key column list.
*/
private $bMetaDataSet = FALSE;
public function getMetaData()
{
$oMetaData = parent::getMetaData();
if (!$this->bMetaDataSet) {
$oMetaData->tableSchema->primaryKey = array( 'field', 'option_value' );
$this->bMetaDataSet = TRUE;
}
return $oMetaData;
}
}
The Yii relation():
class Sites extends CActiveRecord
{
// ...
public function relations()
{
return (
'type_option' => array(
self::BELONGS_TO,
'FieldOptionsDistinct',
array(
'type' => 'option_value',
),
'on' => "type_option.field = 'sites.type'",
),
);
}
}
And all that does the trick. Easy, right?!?

How to use the cascade_delete config option in CodeIgniter Datamapper

I use MySQL using InnoDB tables with CodeIgniter Datamapper in my PHP application. Often, the user is given the option of deleting a record through the app by initiating a ->delete function call. When a record has child records (one-to-one or one-to-many), I would also like these records to be deleted along with the parent record, if it is stated by FK constraints in the database.
In this case, I have 2 tables, items and input_lines. I have confirmed that both are using InnoDB. Each item can have many input_lines, so input_lines has a field called item_id, which is set to NULL, indexed, and have FK constraints (ON CASCADE DELETE and ON CASCADE UPDATE). I have set the config element in the DM config file as
$config['cascade_delete'] = FALSE
Because in the documentation it says you should do that if you are using ON UPDATE/DELETE CASCADE. However, when the user initiates the $item->delete() method, only the item is deleted, and the item_id fields on the input_line records associated with the item are set to null.
My models look like this:
class Item extends DataMapper {
public $has_many = array('labour', 'item_type', 'input_line', 'custom_item_type');
...
}
class Input_line extends DataMapper {
public $has_one = array('item');
...
}
I have tried this with cascade_delete = false and true and it won't work. I know the constraints work because deleting the record with MySQL directly works as expected, deleting the child records.
What am I missing? Why is it setting the FK fields to null instead of deleting the record?
EDIT 1:
I decided against my better judgment to debug the delete function in datamapper.php (libraries directory).
I noticed this code in that function:
// Delete all "has many" and "has one" relations for this object first
foreach (array('has_many', 'has_one') as $type)
{
foreach ($this->{$type} as $model => $properties)
{
// do we want cascading delete's?
if ($properties['cascade_delete'])
{
....
So I var_dumped the contents of $properties, and I saw this:
array (size=8)
'class' => string 'labour' (length=6)
'other_field' => string 'item' (length=4)
'join_self_as' => string 'item' (length=4)
'join_other_as' => string 'labour' (length=6)
'join_table' => string '' (length=0)
'reciprocal' => boolean false
'auto_populate' => null
'cascade_delete' => boolean true
It appears the default for when the model doesn't have the property specifically initialized is overriding the config value. This seems like too glaring a mistake so there's definitely something I'm doing wrong somewhere...right? I really, really want to avoid hacking the DM core files...
EDIT 2:
I was thinking maybe the config file wasn't being found, but I checked the logs and there're entries stating that the Datamapper config file was successfully loaded, so that's not the issue.
Doesn't look like anyone has any answers.
My solution was to change the property in the datamapper library $cascade_delete to false, since it's set to true right now. It's unfortunate that I have to resort to hacking the core, but DM won't respect my changes in the config file for cascade_delete so I have no other choice.
If anyone comes across this question and has encountered an issue like this before, please comment.
I have come across the same problem, but finally I just did like this:
$sql = "DELETE FROM EVENT WHERE event_id=".$event_id.";";
$this->db->query ($sql );
In case we set "ON DELETE CASCADE" for the foreign key which refers to event_id, the above SQL works fine, so I just call it directly.

belongsToMany relationship in Laravel across multiple databases

I have model A and model B which lie in two different databases.
Now I have a pivot_table called a_bs in the same database as model A.
I've setup the belongsToMany relatinoship like this in model A
public function bs()
{
return $this->belongsToMany('B', 'a_bs', 'a_id', 'b_id');
}
When I try to access this relationship like so:
$a = A::find($id);
print_r($a->bs->lists('id'));
I get an error that my pivot table doesn't exist in model B's database. Which is obviously correct since the pivot table is in model A's database. How can I let Laravel know that?
Do not suggest to put the pivot table in model B's database
Very simply:
public function bs()
{
$database = $this->getConnection()->getDatabaseName();
return $this->belongsToMany('B', "$database.a_bs", 'a_id', 'b_id');
}
I'm obtaining the database name dynamically because my connection is configured based off an environment variable. Laravel seems to assume the pivot table to exist in the same database as the target relation, so this will force it to look instead to the database corresponding to the model that this method is in, your 'A' realm.
If you're not worried about SQLite databases, i.e. in the scope of a unit-test, that's all you need. But if you are, keep reading.
Firstly, the previous example isn't sufficient on its own. The value of $database would end up being a file-path, so you need to alias it to something that won't break an SQL statement, and make it accessible to the current connection. "ATTACH DATABASE '$database' AS $name" is how you do that:
public function bs()
{
$database = $this->getConnection()->getDatabaseName();
if (is_file($database)) {
$connection = app('B')->getConnection()->getName();
$name = $this->getConnection()->getName();
\Illuminate\Support\Facades\DB::connection($connection)->statement("ATTACH DATABASE '$database' AS $name");
$database = $name;
}
return $this->belongsToMany('B', "$database.a_bs", 'a_id', 'b_id');
}
Warning: Transactions muck this up: If the current connection is using transactions, the ATTACH DATABASE statement will fail. You can use transactions on it after executing that statement though.
Whereas, if the related connection uses transactions, the resulting data will be silently rendered invisible to the current one. This drove me nuts for longer than I'd care to admit, because my queries ran without error, but kept coming up empty. It seems only data truly written to the attached database is actually accessible to the one it's attached to.
So, after being forced to write to your attached database, you may still want your test to clean up after itself. A simple solution there would be to just use $this->artisan('migrate:rollback', ['--database' => $attachedConnectionName]);. But if you have multiple tests that need the same tables, this is not very efficient, as it forces them to have to rebuild them each time.
A better option would be to truncate the tables, but leave their structure in tact:
//Get all tables within the attached database
collect(DB::connection($database)->select("SELECT name FROM sqlite_master WHERE type = 'table'"))->each(function ($table) use ($name) {
//Clear all entries for the table
DB::connection($database)->delete("DELETE FROM '$table->name'");
//Reset any auto-incremented index value
DB::connection($database)->delete("DELETE FROM sqlite_sequence WHERE name = '$table->name'");
});
}
This will wipe all data from that connection, but there's no reason you couldn't apply some kind filter to that however you see fit. Alternatively, you could take advantage of the fact that SQLite DBs are easily-accessible files, and just copy the attached one to a temp file, and use it to overwrite the source after the test is done executing. The result would be functionally identical to a transaction.
You can set the database of the table in the model class:
protected $table = 'A.a_s';
And You have to use singular form when create a pivot table.
/app/model/A.php
class A extends Eloquent {
// Set table name (plural) with database name
protected $table = 'A.a_s';
// Many to many relation
public function b_s() {
return $this->belongsToMany('B');
}
}
/app/model/B.php
class B extends Eloquent {
// Set table name (plural) with database name
protected $table = 'B.b_s';
}
Query
print_r(A::with('b_s')->where('id', 1)->get()->toArray());
MySQL
CREATE TABLE `A`.`a_s` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
PRIMARY KEY (`id`)
);
CREATE TABLE `B`.`b_s` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
PRIMARY KEY (`id`)
);
CREATE TABLE `A`.`a_b` (
`a_id` int(10) unsigned NOT NULL,
`b_id` int(10) unsigned NOT NULL,
PRIMARY KEY (`a_id`,`b_id`)
) ENGINE=InnoDB;
INSERT INTO A.a_s VALUES (NULL);
INSERT INTO A.a_s VALUES (NULL);
INSERT INTO B.b_s VALUES (NULL);
INSERT INTO A.a_b VALUES (1,1);
INSERT INTO A.a_b VALUES (1,2);
If the databases/schemas are on the same host server like #NiRR said just do this to override the default schema of the second connection:
return $this->belongsToMany('B', 'real-schema-name.a_bs');
or alternatively
return $this->belongsToMany('A', 'real-schema-name.a_bs');
Depending on which model (A or B) is defined with the connection that isn't using the default schema.
Remember that it's impossible to do a join query that spans across two servers; which server is it going to get executed on? Each is missing some of the needed data needed to preform the request.
This is only possible if the two databases are on the same connection (server).
Its not possible to do this on two different connections (servers) since you'll need all three tables on a single server that will perform the join command.
Explicitly setting or defining the connection for both models works just fine.
protected $connection = 'connection1'; //inside model A
protected $connection = 'connection2'; //inside model B
EDIT
When Laravel is fetching a model from the DB, provided a database/connection has been defined (as a property) within the model, Laravel will use the database name for that connection when constructing the SQL. So when working with multiple connections, it is best to define the connection for every model.

Best way to filter access to controller actions according to a specific client id

Using CakePHP 2.2, I am building an application in which each client has it's own "realm" of data and none of the other data is visible to them. For example, a client has his set of users, courses, contractors and jobs. Groups are shared among clients, but they cannot perform actions on groups. All clients can do with groups is assign them to users. So, an administrator (using ACL) can only manage data from the same client id.
All my objects (except groups, of course) have the client_id key.
Now, I know one way to get this done and actually having it working well, but it seems a bit dirty and I'm wondering if there is a better way. Being early in the project and new to CakePHP, I'm eager to get it right.
This is how I'm doing it now :
1- A user logs in. His client_id is written to session according to the data from the user's table.
$user = $this->User->read(null, $this->Auth->user('id'));
$this->Session->write('User.client_id', $user['User']['client_id']);
2- In AppController, I have a protected function that compares that session id to a given parameter.
protected function clientCheck($client_id) {
if ($this->Session->read('User.client_id') == $client_id) {
return true;
} else {
$this->Session->setFlash(__('Invalid object or view.'));
$this->redirect(array('controller' => 'user', 'action' => 'home'));
}
}
3- Im my different index actions (each index, each relevant controller), I check the client_id using a paginate condition.
public function index() {
$this->User->recursive = 0;
$this->paginate = array(
'conditions' => array('User.client_id' => $this->Session->read('User.client_id'))
);
$this->set('users', $this->paginate());
}
4- In other actions, I check the client_id before checking the HTTP request type this way.
$user = $this->User->read(null, $id);
$this->clientCheck($user['User']['client_id']);
$this->set('user', $user);
The concept is good - it's not 'dirty', and it's pretty much exactly the same as how I've handled situations like that.
You've just got a couple of lines of redundant code. First:
$this->Auth->user('id')
That method can actually get any field for the logged in user, so you can do:
$this->Auth->user('client_id')
So your two lines:
$user = $this->User->read(null, $this->Auth->user('id'));
$this->Session->write('User.client_id', $user['User']['client_id']);
Aren't needed. You don't need to re-read the User, or write anything to the session - just grab the client_id directly from Auth any time you need it.
In fact, if you read http://book.cakephp.org/2.0/en/core-libraries/components/authentication.html#accessing-the-logged-in-user it even says you can get it from outside the context of a controller, using the static method like:
AuthComponent::user('client_id')
Though it doesn't seem you'll be needing that.
You could also apply the client_id condition to all finds for a Model by placing something in the beforeFind function in the Model.
For example, in your User model, you could do something like this:
function beforeFind( $queryData ) {
// Automatically filter all finds by client_id of logged in user
$queryData['conditions'][$this->alias . '.client_id'] = AuthComponent::user('client_id');
return $queryData;
}
Not sure if AuthComponent::user('client_id') works in the Model, but you get the idea. This will automatically apply this condition to every find in the model.
You could also use the beforeSave in the model to automatically set that client_id for you in new records.
My answer may be database engine specific as I use PostgreSQL. In my project I used different schema for every client in mysql terms that would be separate database for every client.
In public schema (common database) I store all data that needs to be shared between all clients (objects that do not have client_id in your case), for example, variable constants, profile settings and so on.
In company specific models I define
public $useDbConfig = 'company_data';
In Controller/AppController.php beforeFilter() method I have this code to set schema according to the logged in user.
if ($this->Session->check('User.Company.id')) {
App::uses('ConnectionManager', 'Model');
$dataSource = ConnectionManager::getDataSource('company_data');
$dataSource->config['schema'] =
'company_'.$this->Session->read('User.Company.id');
}
As you see I update dataSource on the fly according to used company. This does exclude any involvement of company_id in any query as only company relevant data is stored in that schema (database). Also this adds ability to scale the project.
Downside of this approach is that it creates pain in the ass to synchronize all database structures on structure change, but it can be done using exporting data, dropping all databases, recreating them with new layout and importing data back again. Just need to be sure to export data with full inserts including column names.

Categories