I have a Symfony 3.4 app that could contain multiple companies.
Each company have their own config, and their own data in db, so I need that each company have their own db.
When any user login, The application has a "core database" containing user's info.
After user login the application must change configuration for connect to user company database, that had saved in "core database".
There are necessary steps:
One user enter his user and password
the app look into central db and get user's authentication.
The app get user configuration to change.
The app change the configuration and now, sql request will be to the company's db.
It is possible? If not, is there any alternative?
Thank you so much!
You have to work here with multiple entity managers and connections and and idea is to use a subscriber that retrieves the current customer based on the user. This subscriber (or another service) will set a global variable containing the name of the entity manager.
// A subscriber (high level priority) or a service already set $customerName
// In your controller or in a service
$customerEntityManager = $this->getDoctrine()->getManager($customerName);
Check also this bundle for ideas https://github.com/vmeretail/multi-tenancy-bundle
Edit
Use and adapt to your needs this file https://github.com/vmeretail/multi-tenancy-bundle/blob/master/Service/TenantResolver.php
Here you just need to resolve tenant from the current User.
In your controller:
...
public function index(TenantResolver $tenantResolver)
{
$customerEntityManager = $this->getDoctrine()->getManager($tenantResolver->getTenant()->getName()); // or getId() or something else
}
In a service:
...
use Doctrine\Common\Persistence\ManagerRegistry;
private $tenantResolver;
private $managerRegistry;
public function__construct(TenantResolver $tenantResolver, ManagerRegistry $managerRegistry)
{
$this->tenantResolver = $tenantResolver;
$this->managerRegistry = $managerRegistry;
}
public function doSomething()
{
$this->managerRegistry->getManager($this->tenantResolver->getTenant()->getName()); // or getId() or something else
}
It's the idea, there must be something better to do here like injecting directly the right manager in the service/controller constructor.
I found the following solution for Symfony 4 and i think it should work for symfony 3.4 as well.
I created a service that copies the default entity manager in a new one connecting to another database:
namespace App\Service;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
use Doctrine\ORM\EntityManagerInterface;
class CustomEntityManagerHelper
{
private $em;
public function __construct(EntityManagerInterface $entityManager)
{
$this->em = $entityManager;
}
/*
* get entity manager for another database
*/
public function getManagerForDatabase($db_name): EntityManagerInterface
{
$conn = array(
'driver' => 'pdo_mysql',
'user' => 'root',
'password' => 'mypass',
'dbname' => $db_name
);
return \Doctrine\ORM\EntityManager::create(
$conn,
$this->em->getConfiguration(),
$this->em->getEventManager()
);
}
}
Until now it was very easy but the Repository class still uses the default entitymanager. So i added a method setEntityManager to the Repositories:
<?php
namespace App\Repository;
use App\Entity\Product;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Common\Persistence\ManagerRegistry;
use Doctrine\ORM\EntityManagerInterface;
class ProductRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Product::class);
}
public function setEntityManager(EntityManagerInterface $entityManager): self
{
$this->_em = $entityManager;
return $this;
}
// custom methods here
}
Now i can use the custom entity manager AND set that to the repository:
use App\Service\CustomEntityManagerHelper;
// ...
/**
* #Route("/products", name="app_product", methods={"GET"})
*/
public function index(CustomEntityManagerHelper $helper): Response
{
$myManager = $helper->getManagerForDatabase($this->getUser()->getDatabaseName());
$products = $myManager->getRepository('App:Product')
->setEntityManager($myManager) // IMPORTANT!
->findAll();
return $this->render('product/index.html.twig', [
'products' => $products
]);
}
Related
I open one database at the start, then need to open another database based on user selecting two values. The database selection has to be at run-time and will change every time.
Have tried to access the Connection String using the Connection String class and have tried other options like Singleton which I do not understand. I am running this on a local Windows 10 system running SQL Server Express. Am coding using Asp.Net Core 2.1
> ASP.Net Core v2.1
Building multi tenant, multi year application
Every client will have one SQL DATABASE per year
I hope to have a table with the following structure
COMPANY_CODE VARCHAR(3),
COMPANY_YEAR INT,
COMPANY_DBNAME VARCHAR(5)
Sample Data
COMPANY_CODE: AAD
COMPANY_YEAR: 19
COMPANY_DB: AAD19
COMPANY_CODE: AAD
COMPANY_YEAR: 18
COMPANY_DB: AAD18
COMPANY_CODE: AAD
COMPANY_YEAR: 17
COMPANY_DB: AAD17
So, every company will multiple rows - one for each financial year.
The COMPANY_DB column will store the DB name to open for that session.
Once the user is authenticated, I want to change the connection string to point to the database in the COMPANY_DB column of the selected row and then let the logged in user perform transactions.
I am unable to figure out how to change the connection string that is embedded in startup.cs.
Any tips on how to achieve this will be most appreciated.
I figured out that you are using one DbContext class for each database. See here for more information: docs.
Remove AddDbContext from Startup, remove OnConfiguring from DbContext and pass options to the constructor.
public class BloggingContext : DbContext
{
public BloggingContext(DbContextOptions<BloggingContext> options)
: base(options)
{ }
public DbSet<Blog> Blogs { get; set; }
}
Then, write service providing DbContext:
public interface IBlogContextProvider
{
BlogContext GetBlogContext(string connectionString);
}
public class BlogContextProvider : IBlogContextProvider
{
BlogContext GetBlogContext(string connectionString)
{
var optionsBuilder = new DbContextOptionsBuilder<BloggingContext>();
optionsBuilder.UseSqlServer(connectionString);
return new BlogContext(optionsBuilder);
}
}
Add service in your Startup.cs:
services.AddScoped<IBlogContextProvider, BlogContextProvider>();
Now you can use DI:
public class HomeController : Controller
{
private IBlogContextProvider _provider;
public HomeController(IBlogContextProvider provider)
{
_provider = provider;
}
public ActionResult Index()
{
using (var context = _provider.GetBlogContext(<your connection string>))
{
//your code here
}
return View();
}
}
EDIT: Of course, you can write ContextProvider as generic.
I would have to make many modifications to be able to transfer my laravel project with a single database that has many query builders and eloquent to a project that supports more than one database?
I understand that once a new database is installed it is necessary to use:
connection('mysql2')
When consulting a database, do we tend to change the whole project with this sentence? specifying the connection in each place?
You can add a $connection property to your Eloquent models to specify the database connection there. This way you don't need to update your queries.
protected $connection = 'connection-name';
Migration with multiple connections
public function up()
{
Schema::connection('mysql-2')->create('user_details', function (Blueprint $table) {
//........
});
}
public function down()
{
Schema::connection('mysql-2')->dropIfExists('user_details');
}
Handle Relationship with multiple database connections
UserDetail.php //mysql-2 (connection-2)
class UserDetail extends Model
{
protected $connection = 'mysql-2';
public function user()
{
return $this->setConnection('mysql')
->belongsTo(User::class);
}
}
User.php //mysql (connection-1) //default connection
class User extends Model
{
//with default connection
public function detail()
{
return $this->setConnection('mysql-2')
->hasOne(UserDetail::class);
}
}
You don't need to a specified connection in a controller for retrieving/delete/insert data
I Understand that you are asking basically how to change database.
For whole project: you can edit mysql connection details in your .env file.
You can also use 2 databases with 1 project , you can learn how to do that from this question which has been already answered: How to use multiple databases in Laravel
I am sorry if i didn't understand your question.
Let me know if it helps you.
I have a CakePHP application hosted on AWS Elastic Beanstalk. Because of the multiple EC2 instances I will use in the future I want to store my PHP sessions in a database. AWS provides a very nice library for storing PHP sessions in their DynamoDB database. See http://goo.gl/URoi3s
Now I putted the AWS SDK in my vendors folder and created an access wrapper for it (a plugin):
<?php
Configure::load('aws');
require_once VENDORS . 'autoload.php';
use Aws\Common\Aws;
class AwsComponent extends Component
{
private $_aws;
public function __construct()
{
$this->_aws = Aws::factory(array(
'key' => Configure::read('Aws.key'),
'secret' => Configure::read('Aws.secret'),
'region' => Configure::read('Aws.region')
));
}
public function getClient($service)
{
return $this->_aws->get($service);
}
}
The wrapper is working well, I already implemented some S3 stuff. Now for the session handler i added the following code to my AppController.php:
public $components = array('Aws.Aws');
public function beforeFilter()
{
$this->_setSessionStorage();
}
private function _setSessionStorage()
{
$client = $this->Aws->getClient('dynamodb');
$client->registerSessionHandler(array(
'table_name' => 'sessions'
));
}
The AWS's internal registerSessionHandler() is executed (tested it) but the session is not beeing stored into the DynamoDB table. Of course I created the table before and if I add the call to the AWS library directly to my webroot/index.php before dispatcher is loaded everything works fine.
I think the problem is that my code is executed after CakePHP calls session_start(). So what is the best way to implement that? http://goo.gl/kUFUIR doesn't help me, I don't want to rewrite the AWS library for beeing compatible with the CakePHP interface.
So what is the best way to implement that? http://goo.gl/kUFUIR
doesn't help me, I don't want to rewrite the AWS library for beeing
compatible with the CakePHP interface.
This is in fact the best way. And this does not mean to reinvent the wheel, abstraction in OOP means that you make things available in a generic interface that can be replaced with something else. You wrap a foreign API or code in an API compatible to your system, in this case a CakePHP application.
Wrap the vendor lib in a AwsSession adapter that implements the CakeSessionHandlerInterface. This way it's API compatible with other session adapters in the case you change it and it might be even solve your core problem, because CakeSession will take care of the initialization.
Your component is initialized after the session in CakePHP, when the controller is already instantiated and then is initializing all its components. So this happens at a pretty late time. Your alternative is to stop CakePHP from initializing the session, I never had a need to do so, so no idea without looking it up myself. Dig in CakeSession. Even if you manage to do so, other components like the default Auth adapter depends on being able to work with Sessions, so you have to take care of the issue that your component has to be loaded before Auth as well. Pretty fragile system with lots of possbile points of failure. Seriously, go for the Session adapter, guess its a lot less painful to get it working this way.
By a quick look at the DynamoDB Session documentation this seems to be pretty easy. Extend the regular session handler and overload only the init and garbage collection of it to add the Aws API calls there, no guarantee this is right but seems to be easy.
What I end up with in CakePHP 3.
src/Network/Session/DynamoDbSession.php
<?php
namespace App\Network\Session;
use Aws\DynamoDb\DynamoDbClient;
use Cake\Core\Configure;
class DynamoDbSession implements \SessionHandlerInterface
{
private $handler;
/**
* DynamoDbSession constructor.
*/
public function __construct()
{
$client = new DynamoDbClient(Configure::read('DynamoDbCredentials'));
$this->handler = $client->registerSessionHandler(array(
'table_name' => Configure::read('DynamoDbCredentials.session_table')
));
}
public function close()
{
return $this->handler->close();
}
public function destroy($session_id)
{
return $this->handler->destroy($session_id);
}
public function gc($maxlifetime)
{
return $this->handler->gc($maxlifetime);
}
public function open($save_path, $session_id)
{
return $this->handler->open($save_path, $session_id);
}
public function read($session_id)
{
return $this->handler->read($session_id);
}
public function write($session_id, $session_data)
{
return $this->handler->write($session_id, $session_data);
}
}
Activate it in config/app.php file:
'Session' => [
'defaults' => 'php',
'handler' => [
'engine' => 'DynamoDbSession'
],
'timeout' => (30 * 24 * 60)
]
I’m trying to create a custom datasource for Amazon Web Services in CakePHP. My approach is as follows:
Base AwsDataSource that creates signatures, makes the actual HTTP requests etc
Various datasources for each AWS product (i.e. S3, SQS etc) that extend this class and specifies the endpoint to use
Models for things like S3Bucket, SqsQueue, SqsMessage and so on
My base datasource class looks like this (simplified):
<?php
class AwsDataSource extends DataSource {
public $config = array(
'key' => '',
'secret' => '',
'region' => ''
);
public $endpoint;
public function signRequest($parameters) {
// generates signature
}
public function makeRequest($parameters = array(), $method = 'get') {
// generates signature and makes HTTP request to AWS servers
}
}
And a sample model looks like this:
<?php
class SqsQueue extends AwsAppModel {
public $name = 'SqsQueue';
public $useTable = false;
}
My problem comes trying to then use these models/datasources in my CakePHP app.
I’ve implemented methods named create(), read(), update() and delete() in my AWS datasource as per the CakePHP cookbook, but they don‘t seem to be getting called. I know this because I’ve put die() statements in my datasource with a message, and execution is never stopped.
I’ve exhausted the cookbook, so if any one could show me how to get my models to call the CRUD methods in my datasource classes then I’d be most grateful.
My bad. Turns out my approach was flawed.
The datasource is specified in database config and is specified as AwsDataSource. Therefore, S3DataSource or SqsDataSource is never used, even though that’s where I’ve defined my CRUD methods, hence my application never exiting (because the CRUD methods aren’t defined in AwsDataSource, the actual datasource being called).
Looks like it’s back to the drawing board.
I'm currently using StructureMap to inject an NHibernateRegistry instance into my DAL, which configures NHibernate for a single connection string and bootstraps a Singleton FluentConfiguration for my single-user app.
How should I modify my Fluent NHibernate configuration to use a different database based on a {tenant} routing parameter in my routing URL?
Routing example:
{tenant}/{controller}/{action}/{id}
...where requests for branch1/Home/Index and branch2/Home/Index use the same application code, but different databases to retrieve the data displayed.
I solved this problem in the past for StructureMap and LINQ by injecting a per-request TenantContext object, which retrieved the routing parameter from the HttpContext it accepted as a constructor parameter and specified a different LINQ data context.
However, I suspect NHibernate has a better of handling this than I could cook up.
Partial NHibernateRegistry class
public class NHibernateRegistry : Registry
{
// ... private vars here
public NHibernateRegistry()
{
var cfg = Fluently.Configure()
.Database(MsSqlConfiguration
.MsSql2008.ConnectionString(c =>
c.FromConnectionStringWithKey("TenantConnectionStringKey")))
// where to inject this key?
.ExposeConfiguration(BuildSchema)
.Mappings(x =>
x.FluentMappings.AddFromAssembly(typeof(UserMap).Assembly)
For<FluentConfiguration>().Singleton().Use(cfg);
var sessionFactory = cfg.BuildSessionFactory();
For<ISessionFactory>().Singleton()
.Use(sessionFactory);
For<ISession>().HybridHttpOrThreadLocalScoped()
.Use(x => x.GetInstance<ISessionFactory>().OpenSession());
For<IUnitOfWork>().HybridHttpOrThreadLocalScoped()
.Use<UnitOfWork>();
For<IDatabaseBuilder>().Use<DatabaseBuilder>();
}
}
StructureMap configuration:
public static class Bootstrapper
{
public static void ConfigureStructureMap()
{
ObjectFactory.Initialize(Init);
}
private static void Init(IInitializationExpression x)
{
x.AddRegistry(new NHibernateRegistry()); // from Data project
}
}
I'm new to NHibernate, so I am unsure of scoping my sessions and configurations. Does NHibernate have a built-in way to handle this?
This worked for me in an a module
return Fluently.Configure()
.Database(MsSqlConfiguration.MsSql2008.ConnectionString(x => x.FromConnectionStringWithKey("IMB"))
.Cache(c => c.UseQueryCache().QueryCacheFactory<StandardQueryCacheFactory>()
.RegionPrefix("IMB")
.ProviderClass<HashtableCacheProvider>()
.UseMinimalPuts()).UseReflectionOptimizer())
.Mappings(m => m.FluentMappings.AddFromAssembly(Assembly.Load("IMB.Data")))
.Mappings(m => m.FluentMappings.AddFromAssembly(Assembly.Load("IMB.Security")))
.ExposeConfiguration(
c => c.SetProperty("current_session_context_class", "web"))
.ExposeConfiguration(cfg => _configuration = cfg)
.BuildSessionFactory();
The problem is that you really want your ISessionFactory object to be a singleton. This means its best not to specify the connection string when creating the ISessionFactory. Have your tried creating the ISessionFactory without specifying a connection string and then passing a manually created connection to ISessionFactory.OpenSession?
For example:
public ISession CreateSession()
{
string tennantId = GetTennantId();
string connStr = ConnectionStringFromTennant(tennantId);
SqlConnection conn = new SqlConnection(connStr);
conn.Open();
session = sessionFactory.OpenSession(conn);
}
And then tell StructureMap to call this method.
The downside is that you can't now from build the database schema when creating the ISessionFactory, but maybe creating database schemas in web applications isn't that great an idea anyway?