CakePHP 4 - customising the query used by the Authentication component - php

On a CakePHP 4.3.5 website I'm using the Authentication and Authorization components mentioned in the documentation:
Authentication
Authorization
These supersede the old AuthComponent that was used in previous versions of CakePHP. This is mentioned in the docs:
Deprecated since version 4.0.0: The AuthComponent is deprecated as of 4.0.0 and will be replaced by the authorization and authentication plugins.
My question is related to the new Authentication component. I want to customise the query that I use when a user attempts to login.
I have 2 columns in my users table called email_verified and banned. By default in my application both of these fields are NULL in the database. So when a user creates an account both of these have NULL values. If/when a user verifies their email address, email_verified gets updated to a tinyint(1) with a value 1. The banned field remains NULL unless the user gets banned by an administrator, at which point it would also be updated to 1.
Thus I want my login query to find Users where email_verified => 1 and banned => NULL.
In the documentation - for what I assume is the "old" AuthComponent way of doing things - it has an example. The only difference in the example is that the query looks at a database column called active however I'm comfortable with how to change that to use the 2 columns I've mentioned above.
The problem is that this doesn't work and I assume it's because the new components work differently. There seems to be a lack of documentation about how to get it to work.
What I tried was as follows:
// src/Controller/AppController.php
public function initialize(): void
{
parent::initialize();
$this->loadComponent('Authorization.Authorization', [
'authenticate' => [
'Form' => [
'finder' => 'auth'
]
// ...
}
I then added a method to my UsersTable.php model file as per the documentation, substituting the appropriate fields:
// src/Model/Table/UsersTable.php
public function findAuth(\Cake\ORM\Query $query, array $options)
{
$query
->select(['id', 'email', 'password', 'email_verified', 'banned'])
->where(['Users.email_verified' => 1, 'Users.banned' => null]);
return $query;
}
This doesn't work. Any user - with the correct username/password combination - can login, irrespective of the values of email_verified or banned. I even tried changing these to non-existent columns (e.g. Users.email_verified_foo_bar_baz) to see if it errored. It doesn't error and I don't think it's even using this function.
How can I get this to work with the new Authorization component in CakePHP 4?

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?

Backpack for Laravel - Permissionmanager: Can't create new user

I have the following problem. I am using Backpack for Laravel with the PermissionManager Extension. By default, you have to log in via your email address. However, you can change that to log in with a username. Works fine if I (for example) seed the database. But for some strange reason, I can not create new users via the PermissionManager PermissionManager Extension. If I try the following error message is the result:
SQLSTATE[HY000]: General error: 1364 Field 'name' doesn't have a
default value
(SQL: insert into users (updated_at, created_at)
values (2018-11-29 22:26:32, 2018-11-29 22:26:32))
Just for testing, I gave the name column a default value, but the error message is the same, except the error now is:
Field 'username' doesn't have a default value.
It just takes the next column in my database.
Of course, I've changed the CRUD-Requests, Form-Requests and so on, to store a username instead of an email. But it looks like the Request doesn't get my form values. It only tries to store the timestamps.
Strange thing: If I reverse all I did in the Controllers and so on and create an email column again, it will just work fine... did I overlook something?
How can I solve this error and store users with a username instead of an email?
Edit:
This is even odder... The data gets posted.
Edit 2:
After taking a really close look at the error messages I found the following:
As you can see, the array with my filled in data is still here.
However, in the next instance, the parameters array is completely empty as well as the $instance array.
Step 1: In order to using username you have to edit app/Http/Controllers/Auth/LoginController and add a new method called username
public function username(){
return 'username';
}
Step 2: In User model make sure you use AuthenticatesUsers trait and add username field in fillable array.
Step 3: Make sure in app/Http/Controllers/Auth/RegisterController you add the username validation and add username field in create method.
protected function create(array $data)
{
return User::create([
'name' => $data['name'],
'username' => $data['username'],
'email' => $data['email'],
'password' => Hash::make($data['password']),
]);
}
That's all. Hope it will solve your problem.
When this issue happened for me, I think I caused it by running php artisan vendor:publish instead of following the instructions and running php artisan vendor:publish --provider="Backpack\PermissionManager\PermissionManagerServiceProvider first.
My config/laravel-permission.php file didn't just have the permission and user models incorrect, but the entire file was spatie's version instead of Laravel Backpack's version. My guess is that spatie's vendor:publish register typically runs before Laravel Backpack PermissionManager's.
Never used this package before but I reviewed this package's files from github for solving your problem, i saw some thing in the UserCrudController.php file. The file provides default user CRUD operations. The create and update form items define via setup method in this controller. But i don't see any define for username field.
as I understand it, your user table has a username and in your model $fillable variable contains username and the user create/update form does not post username field to controller.
Can you try define username form element in to UserCrudController.php file for username, or remove username attribute from $fillable in your model.
I hope these solve your problem.

Using Eloquent's ORM to insert usermeta.id into posts.usermeta_id

I'm working on a user-generated content blog that allows a user to go through the whole upload process before being prompted to sign up. Basic flow: fill out form to pick username/basic info->upload blog post->prompt to sign up with email/password. The purpose of reversing the normal flow is to increase the UX and conversion rate and avoid a wall in the beginning.
Instead of migrating, I've just created the tables manually in PHPmyAdmin. I have 3 relational models: Usermeta->hasOne(App\Mopdels\Post), Post->belongsTo(App\Models\Usermeta), and User->belongsTo(App\Models\Usermeta).
What I'm having trouble with is once the user has created a username and submits the first form to the usermeta table, and then submits the second form to upload their blog post to the post table, it doesn't seem to be attaching the usermeta.id to posts.usermeta_id linking them together. I must be missing something or not attaching it correctly. Here's my StoryController:
<?php
namespace App\Controllers\Story;
use App\Models\Post;
use App\Models\User;
use App\Models\Usermeta;
use App\Controllers\Controller;
use Respect\Validation\Validator as v;
class StoryUploadController extends Controller
{
public function guidance($request, $response)
{
return $this->view->render($response, 'storyupload/guidance.twig');
}
//set up our the Upload Story class so the user can upload their story
//render the view 'uploadstory.twig'
public function getStoryUpload($request, $response)
{
return $this->view->render($response, 'storyupload/upload.twig');
}
// This method is called when the user submits the final form
public function postStoryUpload($request, $response, $id)
{
//set up our validation rules for our complete sign up form
$validation = $this->validator->validate($request, [
'title' => v::stringType()->notEmpty()->length(1, 80),
'body' => v::stringType()->notEmpty()->length(1, 2500),
]);
//if validation fails, stay on story upload page
if ($validation->failed()) {
return $response->withRedirect($this->router>pathFor('storyupload.upload'));
}
$user = Usermeta::find($id)->first();
//We can use our Post Model to send the form data to the database
$post = Post::create([
'title' => $request->getParam('title'),
'body' => $request->getParam('body'),
'category' => $request->getParam('category'),
'files' => $request->getParam('img_path'),
'usermeta_id' => usermeta()->attach($user->id),
]);
//after submit, redirect to completesignup page
return $response->withRedirect($this->router->pathFor('auth.completesignup'));
}
}
I continue to get the error 'usermeta_id cannot be null' so it's definitely not pulling the id from the usermeta table correctly.
I've used the create() method to send the usermeta data to the table in my Auth controller.
Would it be better to have all of my form submissions in the Auth controller and what is the proper way using my example to make sure that my posts.usermeta_id is linked to my usermeta.id?
The usermeta form is taken care of by my Auth Controller:
//render the view 'signup.twig'
public function getSignUp($request, $response)
{
return $this->view->render($response, 'auth/signup.twig');
}
// This method is called when the user submits the form
public function postSignUp($request, $response)
{
$validation = $this->validator->validate($request, [
'name' => v::notEmpty()->alpha(),
'username' => v::noWhitespace()->notEmpty()->UsernameAvailable(),
'city' => v::notEmpty()->alpha(),
'country' => v::notEmpty()->alpha(),
]);
//if validation fails, stay on signup page
if ($validation->failed()) {
return $response->withRedirect($this->router->pathFor('auth.signup'));
}
$usermeta = Usermeta::create([
'name' => $request->getParam('name'),
'username' => $request->getParam('username'),
'city' => $request->getParam('city'),
'country' => $request->getParam('country'),
'share_location' => $request->getParam('share_location'),
]);
//after submit, redirect to storyupload/guidance
return $response->withRedirect($this->router>pathFor('storyupload.guidance'));
}
I wrote quite a bit here. To jump directly to what I believe will solve your problem, see the "Your Issue" section. The rest is here as an educational exercise.
A Quick Intro to Laravel Relations
As you probably already know, "relations" in Laravel are virtual concepts that are derived from the hard data in the database. Because they are virtual, there is some overlap in the definition of relations.
When you say "Usermeta has one Post" - what this means is that the posts table will have a usermeta_id field.
When you say "Post belongs to Usermeta" - what this means is that the posts table will have a usermeta_id field.
Notice that these two relations map to the exact same field in the exact same table. Declaring one relation will declare the other by simple congruence. "Usermeta has one Post" and "Post belongs to Usermeta" are identical relations.
A Tweak to Your Relations
There's one other relation that share this same schema (the posts table have a usermeta_id field). That is "Usermeta has many Posts". The difference here is not in how the relations are stored to the database, but in how Laravel interprets the relations and in what queries Laravel will run.
When you say "Usermeta has one Post", Laravel will scan the database for the first Post with a matching usermeta_id and return that as an instance of the Usermeta model.
When you say "Usermeta has many Posts", Laravel will scan the database for all matching usermeta_ids and return them as a Collection of Usermeta models. You likely want this second behavior -- otherwise users won't be able to make a second post after they sign up.
Setting the usermeta_id Field
Laravel allows you to set database fields directly through a relationship. See their documentation on inserting related models for details.
Because many relationships are just ciphers for the same underlying schema, there's no need to insert or update a related model both ways. For instance, suppose we had the following two models:
class User extends Eloquent {
public function posts() {
return $this->hasMany("App\Post");
}
}
class Post extends Eloquent {
public function user() {
return $this->belongsTo("App\User");
}
}
In this case, the following two lines of code are identical and you only need to use one of them:
$post->user()->associate($user);
$user->posts()->save($post);
Both of these will have the same effect (setting the user_id field on the posts table)
The reason I mention this is that it looks like you're trying to double-dip in your code. You're using attach() (conceivably to set the usermeta_id) and you're also setting the usermeta_id directly. I've added a side-note on the attach method below - as I don't believe it's the right method, anyway.
To use Laravel's relations, you would want code like the following to set this field:
public function postStoryUpload($request, $response, $id)
{
//set up our validation rules for our complete sign up form
$validation = $this->validator->validate($request, [
'title' => v::stringType()->notEmpty()->length(1, 80),
'body' => v::stringType()->notEmpty()->length(1, 2500),
]);
//if validation fails, stay on story upload page
if ($validation->failed()) {
return $response->withRedirect($this->router>pathFor('storyupload.upload'));
}
$user = Usermeta::find($id)->first();
//We can use our Post Model to send the form data to the database
$post = Post::create([
'title' => $request->getParam('title'),
'body' => $request->getParam('body'),
'category' => $request->getParam('category'),
'files' => $request->getParam('img_path'),
]);
// Set the usermeta_id field
$post->usermeta()->associate($user);
// Save the model so we write changes to the database
$post->save();
//after submit, redirect to completesignup page
return $response->withRedirect($this->router->pathFor('auth.completesignup'));
}
Manually Setting the usermeta_id Field
Instead of using Laravel's relations to set this field, you can set the field manually. This can sometimes be cleaner, but it's less explicit and can lead to minor bugs if you aren't careful. To do this, you need to treat the usermeta_id field like any other field on your model.
$post->usermeta_id = $user->id;
This also works when mass assigning attributes using fill or create like so:
$post = \App\Post::create([
'title' => $title,
'body' => $body,
'usermeta_id' => $user->id
]);
$post->fill([
'title' => $title,
'body' => $body,
'usermeta_id' => $user->id
]);
Note that when manually setting the usermeta_id like this, you do not need to use any relationship methods. The following code is redundant:
$post->usermeta_id = $user->id;
$post->usermeta()->associate($user);
Your Issue (I Believe)
There's a caveat to mass assignment, however. Per the Laravel documentation, mass assignment requires you to fill out the model's fillable or guarded attributes.
This is one of the most common bugs, if not the most common bug, in any Laravel code - and it doesn't throw an obvious error so it's easy to miss. Consider the following model:
class Post extends Eloquent {
private $fillable = ["title", "body"];
}
If you attempt to mass assign the usermeta_id field like so:
$post = \App\Post::create([
'title' => $title,
'body' => $body,
'usermeta_id' => $user->id
]);
Then it will silently fail. No error is thrown and the Post is created but the usermeta_id field will be NULL - because it's not mass assignable. This is fixed by updating your model like so:
class Post extends Eloquent {
private $fillable = ["title", "body", "usermeta_id"];
}
I will repeat again, as I did above, that if using mass assignment like this you do not not need to use the associate or save relationship methods. This would be redundant. Therefore you can just set usermeta_id directly to $user->id without any of the usermeta()->associate() shenanigans.
The Bugs I Mentioned
I mentioned that manually setting the field like this can cause bugs. So let's actually discuss what some of those bugs are now instead of glossing over them.
If you update the relationship field manually, Laravel will be unaware that the two models are related until it reloads the model from the database. Consider the following two chunks of code:
$post = new Post();
$post->usermeta_id = $user->id;
dd( $post->usermeta->name );
$post = new Post();
$post->usermeta()->associate($user);
dd( $post->usermeta->name );
The first code block will fail, throwing the error "cannot read attribute of null object" -- because as far as Laravel is aware, $post->usermeta is NULL. You set $post->usermeta_id, but you didn't set $post->usermeta.
The second code block will work as expected, because by running the associate function it sets both usermeta_id and usermeta.
95% of the time this doesn't really cause any issues, however. If you're using an asynchronous API call to save the post and then a separate asynchronous API call to read the post at a later time, then Laravel will read the post from the database and properly set up the relation automatically when we sees the usermeta_id field is filled out.
Side-note On the attach() Method
Laravel uses different methods for saving different types of relations - because the different relations imply different underlying database fields.
associate: This sets the *_id field on the current model's table. For instance: $post->user()->associate($user) will set the user_id on the posts table
save: This sets the *_id field on the other model's table. For instance: $post->comments()->save($comment) will set the post_id on the comments table
attach: This sets both *_id fields on a linking table for many to many relationships. For instance, if you had a tag system then $post->tags()->attach($tag) would set post_id and tag_id on the post_tags table
It can be a bit tricky to remember which of these three functions you need. In general, there's a direct mapping from relation to function:
hasOne, hasMany --> save
belongsTo --> associate
belongsToMany --> attach

Laravel Auth, custom database & table, {"error":{"type":"ErrorException","message":"Undefined index: password"

Creating a login route & function using Laravel's Auth class and my database. Not using Eloquent due to the large legacy database that must be used.
I know SHA1 shouldn't be used but the thousands of accounts in the database use it and I haven't migrated.
Route::post('login', function() {
$user = array(
'username_c' => Input::get('username'),
'password_c' => sha1(Input::get('password'))
);
if(Input::has('username') && Input::has('password')) {
if(Auth::attempt($user)) {
return json_encode(array('result' => true));
}
}
return json_encode(array('result' => false));
});
ONLY when I enter valid credentials, it spits back this error in the console:
{"error":{"type":"ErrorException","message":"Undefined index: password","file":"\/var\/www\/html\/vendor\/laravel\/framework\/src\/Illuminate\/Auth\/DatabaseUserProvider.php","line":135}}
What could be the problem here? I edited app/config/auth.php and changed the 'table' to my table, and the 'driver' to 'database'
From reading Laravel's docs., that's all I need to change. What's causing this?
If you look at the line that is being referenced, you see this:
$plain = $credentials['password'];
The authentication is expecting a 'password' field to exist in the credentials provided to Auth::attempt, and you're giving it 'password_c' instead, which is why you're getting that error.
The second line in that method is this:
return $this->hasher->check($plain, $user->getAuthPassword());
This is basically checking whether the plain password provided in the input matches the value that is stored in the database. This means that your input key name does not need to match the name of the database column. The name of the database column is determined by the $user->getAuthPassword() method call above. You will need to make the key that you pass to Auth::attempt to be 'password', though, instead of 'password_c'.
But this isn't just going to work, because as you've probably already noticed in the second line above that class is going to expect an actual instance of a User model, and by your own admission you don't have any Eloquent models.
My suggestion would be to go ahead and create the user eloquent model, because I believe you're going to have to. You don't have to turn all of your tables into models, but if you want authentication to work with Laravel's Auth facade then I don't think there's any way around it.
Update
This answer also has a good method for updating your current passwords to use the Laravel hash during authentication attempts with a fallback to using MD5 (the old hash function, in your case you could use sha1).
Update 2
You need to change your authentication logic to look something like this:
Route::post('login', function() {
if(Input::has('username') && Input::has('password')) {
$user = array(
'username' => Input::get('username'),
'password' => sha1(Input::get('password'))
);
if(Auth::attempt($user)) {
return json_encode(array('result' => true));
}
}
return json_encode(array('result' => false));
});
Ensure that your user model's getAuthPassword method returns the name of the password column and the getAuthIdentifier returns the name of the username column.

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