I am trying to switch to and query a external database in a custom Drupal 8 module I have created.
I have added the external database below the native database in settings.php:
// Add second database
$databases['external']['default'] = array(
'database' => 'uconomy_external',
'username' => 'uconomy_admin',
'password' => 'fNjA9kC35h8',
'prefix' => '',
'host' => 'localhost',
'port' => '3306',
'namespace' => 'Drupal\\Core\\Database\\Driver\\mysql',
'driver' => 'mysql',
);
I then have a file named BusinessListingDbLogic.php where I make queries to the database :
<?php
namespace Drupal\business_listing;
use Drupal\Core\Database\Connection;
use Drupal\Core\Database\Database;
/**
* Defines a storage handler class that handles the node grants system.
*
* This is used to build node query access.
*
* This class contains all the logic for interacting with our database
*
* #ingroup business_listing
*/
class BusinessListingDbLogic {
/**
* #var \Drupal\Core\Database\Connection
*/
protected $database;
/**
* #param \Drupal\Core\Database\Connection $connection
*/
public function __construct(Connection $connection) {
$this->database = $connection;
//Database::setActiveConnection('external');
}
/**
* Add new record in table business_listing.
*/
public function add($title, $body, $imageName, $location, $email) {
if (empty($title) || empty($body) || empty($imageName) || empty($location) || empty($email)) {
return FALSE;
}
// add record to business_listing table in database.
$query = $this->database->insert('business_listing');
$query->fields(array(
'title' => $title,
'body' => $body,
'image' => $imageName,
'location' => $location,
'email' => $email
));
return $query->execute();
}
I believe my BusinessListingDbLogic class is registered as a service, my business_listing.services.yml looks as follows:
services:
# Service Name.
business_listing.database.external:
class: Drupal\Core\Database\Connection
factory: 'Drupal\Core\Database\Database::getConnection'
arguments: ['external']
# external database dependent serivce.
business_listing.db_logic:
# Class that renders the service.
# BusinessListingDbLogic contains all the functions we use to interact with the business_listings table
class: Drupal\business_listing\BusinessListingDbLogic
# Arguments that will come to the class constructor.
arguments: ['#business_listing.database.external']
# A more detailed explanation: https://www.drupal.org/node/2239393.
# tags:
# - { name: backend_overridable }
This code works until I try uncomment Database::setActiveConnection('external');
I then get the following error:
The website encountered an unexpected error. Please try again later.Drupal\Core\Database\DatabaseExceptionWrapper: SQLSTATE[42S02]: Base table or view not found: 1146 Table 'uconomy_external.shortcut_set_users' doesn't exist: SELECT ssu.set_name AS set_name
FROM
{shortcut_set_users} ssu
WHERE ssu.uid = :db_condition_placeholder_0; Array
(
[:db_condition_placeholder_0] => 1
)
it looks like the switch is working, but Drupal might be trying to use the external database for its native functionality? I know I also have to switch back to the default database at some point, but I am not sure where to do this?
Any help or advice would be GREATLY appreciate. Kind Regards, Matt
Seems that instead of calling your current connection to set, you need to use the static method Database::setActiveConnection() directly.
Eg. $this->database->setActiveConnection('external') becomes Database::setActiveConnection('external')
Related
I'm having some trouble trying to achieve multiple connection to database in some clean way.
Keep in mind that this is my first symfony project ever, and i'm only a young developer.
In my project, the goal is to be able to select a client, with a specific database, and to connect to the database to be able to export some datas.
I tried to do the solution describe in this post Symfony 3 connection to multiple databases and i tried to generate dynamically an entityManager.
So i created a factory EntityManagerFactory :
Factory\EntityManagerFactory
<?php
namespace App\Factory;
use Doctrine\ORM\Tools\Setup;
use Doctrine\ORM\EntityManager;
use Symfony\Component\Yaml\Yaml;
class EntityManagerFactory {
private $config_db_group;
public function __construct(string $config_db_group) {
$this->config_db_group = $config_db_group;
}
public function createManager($idDb) {
$isDevMode = false;
$config = Setup::createAnnotationMetadataConfiguration(array(__DIR__ . "/src"), $isDevMode);
$connectionConfig = $this->getConfigDb($idDb);
$dbParams = [
'driver' => 'pdo_mysql',
'host' => $connectionConfig['host'],
'username' => $connectionConfig['user'],
'password' => $connectionConfig['password'],
'dbname' => $connectionConfig['db_name']
];
return EntityManager::create($dbParams, $config);
}
private function getConfigDb($idDb) {
$connectionConfig = Yaml::parseFile("$this->config_db_group");
return $connectionConfig[$idDb];
}
}
I have a yaml that describes the connection config :
config\dbgroup.yaml
1:
db_name: "db_name1"
host: "host1"
user: "user1"
password: "password1"
port: "3306"
2:
db_name: "db_name2"
host: "host2"
user: "user2"
password: "password2"
port: "3306"
In my config\services.yaml, i did something that was described in the post.
# Create a service for the factory
App\Factory\EntityManagerFactory:
arguments:
$config_db_group: '%kernel.project_dir%\config\db_group.yaml'
# Use the factory service as the first argument of the 'factory' option
# and the factory method as the second argument
App\Factory\EntityManager:
factory: ['#App\Factory\EntityManagerFactory', 'getManager']
I don't really understand what this is, i think this defines my factory as a service ? ...
And then i try to create an entityManager in my controller, this was just to test if it works, i think database request should be in a Repository, or a Services ?
<?php
namespace App\Controller;
use Twig\Environment;
use App\Factory\EntityManagerFactory;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use App\Repository\Istrator\DatabaseGroupRepository;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Query\ResultSetMapping;
use Doctrine\ORM\Query\ResultSetMappingBuilder;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
class DashboardController extends AbstractController {
private $twig;
private $databaseGroupRepository;
private $factory;
public function __construct(Environment $twig, DatabaseGroupRepository $databaseGroupRepository, EntityManagerFactory $factory) {
$this->twig = $twig;
$this->databaseGroupRepository = $databaseGroupRepository;
$this->factory = $factory;
}
#[Route('{slug}/dashboard', name: 'app_dashboard')]
public function index(string $slug): Response {
// I get the specific database
$databaseGroup = $this->databaseGroupRepository->findBySlug($slug);
// Then i try to create an entityManager with the correct config
$entityManager = $this->factory->createManager($databaseGroup->getIdDb());
// Then just to try my connection, i create a basic query
$rsm = new ResultSetMapping();
$test = $entityManager->createNativeQuery("USE mydatabase; SELECT * FROM mytable", $rsm)->execute();
return new Response($this->twig->render('dashboard/dashboard.html.twig', [
'controller_name' => 'DashboardController',
]));
}
}
For now it doesn't work.. But i have several questions :
The databases i try to connect are not database with the same database schemes than my actual database. They are external database. Should i create Entities, and repository to manage them ? Or should i just do some connection, some request without entity and repository ?
In the stackoverflow post that i based my code on, there is a second way of doing it, by defining all future connection in a doctrine.yaml. I have a defined number of connection but like 50 or something, should i do this instead of creating entityManager dynamically ?
As you can see, i'm a bit confused right now but if someone could tell me their point of vue, it would be great.
If you need any other information, just tell me !
Thanks in advance
EDIT :
I found the solution, and it was really stupid :
In my EntityManagerFactory, i did this :
$dbParams = [
'driver' => 'pdo_mysql',
'host' => $connectionConfig['host'],
// IT'S NOT USERNAME, IT'S USER ....
'username' => $connectionConfig['user'],
'password' => $connectionConfig['password'],
'dbname' => $connectionConfig['db_name']
];
In the StackOverflow post that is used to create this factory, it was written username, but the correct field was user.
That was my first mistake, my second mistake is that, when i tried to execute my nativeQuery, I created a resultSetMapping empty :
$entityManager = $this->factory->createManager($databaseGroup->getIdDb());
// I did this
$rsm = new ResultSetMapping();
$test = $entityManager->createNativeQuery("USE mydatabase; SELECT * FROM mytable", $rsm)->execute();
// I SHOULD HAVE DONE THIS
$rsm = new ResultSetMappingBuilder($entityManager);
$rsm->addScalarResult('id', 'id');
[... for every field]
$result = $entityManager->createNativeQuery("SELECT id, prenom, nom FROM mytable",$rsm)->execute();
I use addScalarResult because what i get from those databases are not Entities I will keep in my program.
I hope if someone get stuck like me, this could help him/her/etc..
I'm Developing SaaS application using Yii2 with separate DB architecture. I have a problem in login to system by using tenant database.
I need to get tenant database details from common db and establish tenant db connection after entering company id, username and password in login form.
This is my index.php file.
<?php
defined('YII_DEBUG') or define('YII_DEBUG', true);
defined('YII_ENV') or define('YII_ENV', 'dev');
require(__DIR__ . '/_protected/vendor/autoload.php');
require(__DIR__ . '/_protected/vendor/yiisoft/yii2/Yii.php');
$config = require(__DIR__ . '/_protected/config/web.php');
(new yii\web\Application($config));
if (Yii::$app->session->get('company')) :
$appConnection = \app\models\Userdbconnections::find()->where(['company_id' => Yii::$app->session->get('company')])->one();
\Yii::$app->dbDynamic->dsn = "mysql:host=localhost;dbname=$appConnection->dns";
\Yii::$app->dbDynamic->username = $appConnection->user;
\Yii::$app->dbDynamic->password = $appConnection->password;
\Yii::$app->dbDynamic->charset = 'utf8';
endif;
Yii::$app->run(); // this will run the application
?>
From login function after post logging data, auth controller is like this
if ( Yii::$app->request->post() ){
$connection = \app\models\Userdbconnections::find()->where(['company_id'=>Yii::$app->request->post('LoginForm')['company']])->one();
$_SESSION["dsn"] = $connection->dns;
$_SESSION["user"] = $connection->user;
$_SESSION["pass"] = $connection->password;
$_SESSION["company_id"] = $connection->company_id;
// Yii::$app->db()->close();
Yii::$app->set('db', [
'class' => '\yii\db\Connection',
'dsn' => "mysql:host=localhost;dbname={$connection->dns}",
'username' => $connection->user,
'password' => $connection->password,
]);
$model_db = new LoginForm();
$model_db->load(Yii::$app->request->post());
$model_db->login();
$_SESSION["login_user"] = $model_db->username;
}
User Management Module called in web.php under component part as following
'user' => [
'class' => 'webvimark\modules\UserManagement\components\UserConfig',
// Comment this if you don't want to record user logins
'on afterLogin' => function($event) {
\webvimark\modules\UserManagement\models\UserVisitLog::newVisitor($event->identity->id);
},
'enableSession' =>true,
],
Each model file consist with following code
public static function getDb()
{
return Yii::$app->get('dbDynamic');
}
So now i'm able to log from tenant db. But after checking i noticed User Management part, creation, role creation all these linked to common db when ever i logged in to tenant db. Is there anything I misses in here?
One way to do it is having two connections. One connection for common details coming from common database (user details, tenant db he belongs to, et al). This connection is static, so must be defined in the config (or just rename what comes with Yii Basic app to something like commonDb or use it with just db name.
Another one will be connected to the specific user tenant database. This will be dynamic and details must change. There are many ways to do it. One is to defined it before app runs. See this forum post for details. Another would be setting it up before request using Yii Container and call it inside your models et al. There might be other ways too.
So the process goes like this
User logs in. Connection used is the common connection (let it be defined as Yii::$app->db).
Using details from (1) create the dynamic connection.
Use the connections where needed (in models, Active data providers or Query builders)
Here is untested example
//common database with user login
----------------------------------
| id | name | tenant_database |
----------------------------------
| 1 | Stef | company_a |
----------------------------------
Note here that Yii::$app->user->identity will hold model class that wraps this table
//config/web.php
return [
'components' =>[
'db' => [
'class' => 'yii\db\Connection',
'dsn' => 'mysql:host=localhost;dbname=common_db',
'username' => 'username',
'password' => 'password',
'charset' => 'utf8',
]
'userDb' => [
'class' => 'yii\db\Connection',
'dsn' => 'mysql:host=localhost;dbname=${database}',
'username' => 'username',
'password' => 'password',
'charset' => 'utf8',
]
]
//set it up before request
'on beforeRequest' => function ($event) {
if(Yii::$app->user->isGuest)
{
// redirect user to Login page
}
else
{
$currentDSN = Yii::$app->userDb->dsn;
$tenantDB = Yii::$app->user->identity->tenant_database;
Yii::$app->userDb->dsn = str_replace('${database}', $tenantDB, $currentDSN);
}
},
]
Then in model class override getDb as follows
class Data extends \yii\db\ActiveRecord
{
public static function getDb()
{
return Yii::$app->userDb;
}
}
Then user it as in:
$data = Data::find()->all();
$data = Yii::$app->userDb->createCommand('SELECT * FROM data')->queryAll();
UPDATE
Since OP wants the data to be in tenant db, the only way is having each tenant to have special Tenant Code, and on login page you will provide inputs for Tenant Code, Username and Password. Then
1. Query the common table for the database name associated with that code
2. Change Connection details as shown above
3. Login with TenantLogin class that uses tenant connection as shown above with Data class.
The new common table
----------------------------
| code | tenant_database |
----------------------------
| 12333 | company_a |
----------------------------
I have a problem with using Laravel built in authentication feature. As a default Laravel authentication access users table to check/add username and password. I need to change it to student table. normally in a model protected $table=student code is used to mention which table to use.
Can anyone tell me where the protected $table= code or similar is found within the built in authentication feature?
Part 2
The code below is the code in my controller where I take the form data into $data and validating and returning it into my store function, but I get an error
Type error: Too few arguments to function App\Http\Controllers\StudentController::store(), 0 passed and exactly 1 expected"
protected function validator(array $data)
{
$data = Request::all();
return Validator::make($data, [
'fname' => 'required|string|max:255',
'lname' => 'required|string|max:255',
'district' => 'required|string|max:255',
'email' => 'required|string|email|max:255|unique:student',
'password' => 'required|string|min:6|confirmed',
]);`
}
public function store(array $data)
{
return Student::create([
'fname' => $data['fname'],
'lname' => $data['lname'],
'district' => $data['district'],
'email' => $data['email'],
'password' => bcrypt($data['password']),
]);
Add it in the App\User.php
namespace App;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable
{
use Notifiable;
protected $table = "users_old"; //your custom table
....
}
Add:
protected $table=student;
to User model at app/User.php
and
In app/Http/Controllers/Auth/RegisterController.php,
Change:
'email' => 'required|email|max:255|unique:users',
To:
'email' => 'required|email|max:255|unique:student',
I am trying to use the Laravel inbuilt password reset in my app where Laravel 5.1 acts as the backend api and Angular 1.3 for all front-end views. I have set-up the Password reset as per the docs where I have done the following:
1) Create the table
php artisan migrate
2) Added this to the route:
Route::post('password/email', 'Auth/PasswordController#postEmail');
Route::post('password/reset', 'Auth/PasswordController#postReset');
Since I will be using Angular to display frontend forms, I did not add the views for GET. I havent done any changes to the Auth/PasswordController.php and right now its just like the way it came. But when I test the above URL from Postman POST request, I am getting the error:
View [emails.password] not found.
How can I let Angular Handle the views and not have Laravel worry about the view? Do I have to have Laravel View for the inbuilt password reset to work? How do I approach this?
Override the postEmail and postReset methods so that they return a JSON response (don't let it redirect). Subsequently post to /password/email and /password/reset from Angular via xhr.
Open app/Http/Controllers/Auth/PasswordController.php
<?php namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
class PasswordController extends Controller
{
use ResetsPasswords;
//add and modify this methods as you wish:
/**
* Send a reset link to the given user.
*
* #param \Illuminate\Http\Request $request
* #return \Illuminate\Http\Response
*/
public function postEmail(Request $request)
{
$this->validate($request, ['email' => 'required|email']);
$response = Password::sendResetLink($request->only('email'), function (Message $message) {
$message->subject($this->getEmailSubject());
});
switch ($response) {
case Password::RESET_LINK_SENT:
return redirect()->back()->with('status', trans($response));
case Password::INVALID_USER:
return redirect()->back()->withErrors(['email' => trans($response)]);
}
}
/**
* Reset the given user's password.
*
* #param \Illuminate\Http\Request $request
* #return \Illuminate\Http\Response
*/
public function postReset(Request $request)
{
$this->validate($request, [
'token' => 'required',
'email' => 'required|email',
'password' => 'required|confirmed',
]);
$credentials = $request->only(
'email', 'password', 'password_confirmation', 'token'
);
$response = Password::reset($credentials, function ($user, $password) {
$this->resetPassword($user, $password);
});
switch ($response) {
case Password::PASSWORD_RESET:
return redirect($this->redirectPath());
default:
return redirect()->back()
->withInput($request->only('email'))
->withErrors(['email' => trans($response)]);
}
}
}
Ceckout your path to views folder in app\bootstrap\cache\config.php at section "view"
'view' =>
array (
'paths' =>
array (
0 => '/home/vagrant/Code/app/resources/views',
),
'compiled' => '/home/vagrant/Code/app/storage/framework/views',
),
this path MUST be at SERVER! not at you local mashine like
"D:\WebServers\home\Laravel\app\bootstrap\cache", if you use the homestead.
And You must use command like: "php artisan config:clear | cache" at SERVER!
I had the same problem than you. You could manage to change the view in config/auth.php if you have another one not in resources/views/emails/password.blade.php.
Because this view isn't created by default, that's why you got the error.
Background: Building a web app (as an introduction to CakePHP) which allows users to manage a lounge. A lounge is composed of a blog, contacts, calendar, etc. Each lounge is associated with a subdomain (so jcotton.lounger.local would take you to my lounge). The root of the site, used for creating new lounges, registering users, etc is hosted on lounger.local. I am using Cake 2.0.
Questions:
I wanted to be able to separate actions and views associated with the root site (lounger.local) from individual lounges (subdomains of lounger.local). After a good deal of research I settled on the following soln. I setup a prefix route "lounge" and added the the following code in routes.php. Actions (and views) associated with a lounge all contain the prefix lounge (ex: lounge_index()). How would you handle this?
if(preg_match('/^([^.]+)\.lounger\.local$/',env("HTTP_HOST"),$matches)){
$prefix = "lounge";
Router::connect('/', array('controller' => 'loungememberships','action' => 'index', 'prefix' => $prefix, $prefix => true));
/* Not currently using plugins
Router::connect("/:plugin/:controller", array('action' => 'index', 'prefix' => $prefix, $prefix => true));
Router::connect("/:plugin/:controller/:action/*", array('prefix' => $prefix, $prefix => true));
*/
Router::connect("/:controller", array('action' => 'index', 'prefix' => $prefix, $prefix => true));
Router::connect("/:controller/:action/*", array('prefix' => $prefix, $prefix => true));
unset($prefix);
}
Each time a user performs an action within a lounge such as posting a comment within the blog, adding a contact, etc, it is necessary to lookup the lounge_id (based on the subdomain); this is necessary to verify the user is authorized to perform that action and to associate the corresponding data with the correct lounge. I have implemented this via the beforeFilter function in AppController. Each time a request is received with a subdomain a search is performed and the lounge_id is written to a session variable. Each controller then loads CakeSession and reads the corresponding lounge_id. Is this better than calling ClassRegistry::Init('Lounge') and doing the lookup in each controller? Is there a better soln?
Thanks in advance for the help
The way I approached this was with a custom route, and some trickery with route configuration similar to your example.
First, I have a "Master domain" that is redirected to and used as the main domain for the multi-tenancy site. I also store a default action i want them to take. I store these in configuration variables:
Configure::write('Domain.Master', 'mastersite.local');
Configure::write('Domain.DefaultRoute', array('controller' => 'sites', 'action' => 'add'));
Next, I created a DomainRoute route class in /Lib/Route/DomainRoute.php:
<?php
App::uses('CakeRoute', 'Routing/Route');
App::uses('CakeResponse', 'Network');
App::uses('Cause', 'Model');
/**
* Domain Route class will ensure a domain has been setup before allowing
* users to continue on routes for that domain. Instead, it redirects them
* to a default route if the domain name is not in the system, allowing
* creation of accounts, or whatever.
*
* #package default
* #author Graham Weldon (http://grahamweldon.com)
*/
class DomainRoute extends CakeRoute {
/**
* A CakeResponse object
*
* #var CakeResponse
*/
public $response = null;
/**
* Flag for disabling exit() when this route parses a url.
*
* #var boolean
*/
public $stop = true;
/**
* Parses a string url into an array. Parsed urls will result in an automatic
* redirection
*
* #param string $url The url to parse
* #return boolean False on failure
*/
public function parse($url) {
$params = parent::parse($url);
if ($params === false) {
return false;
}
$domain = env('HTTP_HOST');
$masterDomain = Configure::read('Domain.Master');
if ($domain !== $masterDomain) {
$defaultRoute = Configure::read('Domain.DefaultRoute');
$Cause = new Cause();
if (!($Cause->domainExists($domain)) && $params != $defaultRoute) {
if (!$this->response) {
$this->response = new CakeResponse();
}
$status = 307;
$redirect = $defaultRoute;
$this->response->header(array('Location' => Router::url($redirect, true)));
$this->response->statusCode($status);
$this->response->send();
$this->_stop();
}
$params['domain'] = $domain;
}
return $params;
}
/**
* Stop execution of the current script. Wraps exit() making
* testing easier.
*
* #param integer|string $status see http://php.net/exit for values
* #return void
*/
protected function _stop($code = 0) {
if ($this->stop) {
exit($code);
}
}
}
This custom route class is used within the /Config/routes.php file to setup multi-tenancy.
if (env('HTTP_HOST') === Configure::read('Domain.Master')) {
// Master domain shows the home page.
$rootRoute = array('controller' => 'pages', 'action' => 'display', 'home');
} else {
// Subdomains show the cause view page.
$rootRoute = array('controller' => 'causes', 'action' => 'view', env('HTTP_HOST'));
}
Router::connect('/', $rootRoute, array('routeClass' => 'DomainRoute'));
On inspection of the custom router, you will see that I am pulling the current domain being accessed and adding that to the $params array.
While this does not directly achieve what you are after, minor modifications will get you on the right track with your requirements. There is not a great deal of information about Custom Routes, but here is the CakePHP documentation link for custom route classes.
I hope that helps!