YII2. Change db connection by routing - database

I need to change db connection (prefix) by routing,
When user route to site.com/db1/post system used db1 components config, and when route to site.com/db2/post system used db2
'components' => [
'db1' => [
...,
'tablePrefix' => 'base1_',
],
'db2' => [
...,
'tablePrefix' => 'base2_',
],
...
Can it`s possible?
Maybe have better solution to change db prefix.
I need to use one model with different table (only prefix change)

There are a couple of options. Probably the easiest is using url rules; see http://www.yiiframework.com/doc-2.0/guide-runtime-routing.html#url-rules
rules => [
'<db:db\d>/post' => 'site/post',
]
This will redirect db1/post to site/post with the "db" parameter set to "db1". Then in your SiteController:
public function actionPost($db) {
YourModel::setDb(Yii::$app->$db);
$model = new YourModel();
// do what you need with your model
// and return the rendered result
}
In your model class, you will need to override the getDb() static method, and write a setDb method:
private static $_db;
public static function getDb() {
if (isset(self::$_db)) {
return self::$_db;
}
return ActiveRecord::getDb();
}
public static function setDb($db) {
self::$_db = $db;
}

An easy solution is similar at the advanced template .. where you have two different application with different config section .. in
/db1/config/main.php
you can place the db component setted for accessing to the table prefix you prefer (or also to database you prefer)
'components' => [
'db' => [
...,
'tablePrefix' => 'base1_',
],
and in
/db2/config/main.php
'components' => [
'db' => [
...,
'tablePrefix' => 'base2_',
],
You can use the common namespace for all the common application elemnts you need .. models, controllers ,views and so.. on
and each reseved namespace for specific elements..
Obviuosly this isjust a suggestion .. could be there are others easy and smart solutions
PS: using two separated database you don't need different prefix..

Related

Is there a better way to achieve multiple connection to database in Symfony 6?

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..

How to get authenticated to join private channel using Laravel Echo and Socket.io

I am using Laravel 5.8.10, React 16.8, Laravel Echo Server 1.5.2, Redis 3.2.9, and Socket.io 2.2.0.
I am NOT using Pusher and don't want to use Pusher.
I am trying to create a basic chat system for site users. They log in normally using session authentication with email and password - all of that works fine.
There are 2 types of users: Brands and Influencers. Each has its own custom guard (web-brands & web-influencers). All session guards work normally.
I'm building the chat page using React. I can successfully join a public channel and receive messages on that public channel. However, the problem is when I try to make the channel private.
When I try to join a private channel, Laravel Echo Server sends an authentication request to: http://localhost:8000/broadcasting/auth.
But that returns the following 401 error:
{"message":"Unauthenticated."}
Client can not be authenticated, got HTTP status 401
Right now, I am trying to authenticate requests to /broadcasting/auth using a simple 'api_token' that is stored in the users tables (brands and influencers are the 2 users tables). This is a unique 60-character string.
I am trying this 'api_token' strategy because it sounds easier than setting up Laravel Passport, but perhaps I am wrong about that.
This is the constructor method from my React page:
import React, { Component } from 'react';
import Echo from "laravel-echo";
import Socketio from "socket.io-client";
constructor(props) {
super(props);
this.state = {
currentConversationId: conversations[0].id,
data: '',
};
this.selectConversation = this.selectConversation.bind(this);
let echo = new Echo({
broadcaster: 'socket.io',
host: 'http://localhost:6001',
client: Socketio,
auth: {
headers: {
// I currently have CSRF requirements disabled for /broadcasting/auth,
// but this should work fine once it is enabled anyway
'X-CSRF-Token': document.head.querySelector('meta[name="csrf-token"]'),
// I have the api_token hard-coded as I am trying to get it to work,
// but I have also used the javascript variable 'token' below
'api_token':'uUOyxRgCkVLKvp7ICZ0gXaELBPPbWEL0tUqz2Dv4TsFFc7JO4gv5kUi3WL3Q',
'Authorization':'Bearer: ' +'uUOyxRgCkVLKvp7ICZ0gXaELBPPbWEL0tUqz2Dv4TsFFc7JO4gv5kUi3WL3Q',
//'api_token':token,
//'Authorization':'Bearer: ' + token,
}
}
});
// Note that the ID of 1 is hardcoded for now until I get it to work
echo.private('brand.1')
.listen('SimpleMessageEvent', event => {
console.log('got something...');
console.log(event);
this.state.data = event;
});
}
Here you can see the in $php artisan route:list, the route is using auth:api middleware:
| GET|POST|HEAD | broadcasting/auth | Illuminate\Broadcasting\BroadcastController#authenticate | auth:api
Here is my BroadcastServiceProvider.php:
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Broadcast;
class BroadcastServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*
* #return void
*/
public function boot()
{
Broadcast::routes(['middleware' => ['auth:api']]);
require base_path('routes/channels.php');
}
}
Here is my auth.php:
<?php
return [
'defaults' => [
'guard' => 'web-brands',
'passwords' => 'brands',
],
'guards' => [
'web-brands' => [
'driver' => 'session',
'provider' => 'brands',
],
'web-influencers' => [
'driver' => 'session',
'provider' => 'influencers',
],
'api' => [
'driver' => 'token',
'provider' => 'brands2',
],
],
'providers' => [
'brands' => [
'driver' => 'eloquent',
'model' => App\Brand::class,
],
'influencers' => [
'driver' => 'eloquent',
'model' => App\Influencer::class,
],
'brands2' => [
'driver' => 'database',
'table' => 'brands',
],
],
'passwords' => [
'brands' => [
'provider' => 'brands',
'table' => 'password_resets',
'expire' => 60,
],
'influencers' => [
'provider' => 'influencers',
'table' => 'password_resets',
'expire' => 60,
],
],
];
Here is my channels.php:
Broadcast::channel('brand.{id}',true);
Note that I have the brand.{id} set it to return true by default. I have also tried this for channels.php:
Broadcast::channel('brand.{id}', function ($brand,$id) {
return $brand->id === Brand::find($id)->id;
});
I have already tried testing the simple api_token method by using a dummy route:
Route::get('test-test-test',function(){return 'asdf';})->middleware('auth:api');
This test works:
http://localhost:8000/test-test-test results in redirect
http://localhost:8000/test-test-test?api_token=123 results in redirect
http://localhost:8000/test-test-test?api_token=[the actual correct 60-character token] results in 'asdf'
Here is some info from my .env:
BROADCAST_DRIVER=redis
QUEUE_DRIVER=redis
CACHE_DRIVER=file
QUEUE_CONNECTION=database
SESSION_DRIVER=file
SESSION_LIFETIME=120
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
Here is my laravel-echo-server.json:
{
"authHost": "http://localhost:8000",
"authEndpoint": "/broadcasting/auth",
"clients": [],
"database": "redis",
"databaseConfig": {
"redis": {},
"sqlite": {
"databasePath": "/database/laravel-echo-server.sqlite"
}
},
"devMode": true,
"host": null,
"port": "6001",
"protocol": "http",
"socketio": {},
"sslCertPath": "",
"sslKeyPath": "",
"sslCertChainPath": "",
"sslPassphrase": "",
"subscribers": {
"http": true,
"redis": true
},
"apiOriginAllow": {
"allowCors": false,
"allowOrigin": "",
"allowMethods": "",
"allowHeaders": ""
}
}
Perhaps I am not sending the api_token correctly in the header of the laravel echo request?
UPDATE/EDIT:
Now I have tried removing the auth:api middleware for the /broadcasting/auth route. I'm not sure if that was the correct thing to do.
That now produces a 403 error:
Client can not be authenticated, got HTTP status 403
UPDATE 2 - IMPORTANT
So I know this is not recommended, but I started changing some things inside of the laravel source files... I got it to work finally and now that I have figured it out, I would like to override the source files that I changed instead of actually changing them. I did save the originals so I can easily revert back.
One big challenge was that while changing the source files, I was not able to use the where() method, only the find() method to lookup users.
The key function that needed changing was retrieveUser() (which is located inside of Illuminate/Broadcasting/Broadcasters/Broadcaster.php.
The problem was that it kept trying to run:
return $request->user();
...but that user() function never worked, which is why it always returned a 403 forbidden error. I think it is because the actual Laravel Echo request was sent from React (in javascript frontend), so there was no user object attached to the request. In other words, it was like a guest making the request. That explains why the public channels worked, but the private ones didn't.
I never did figure out how to get the user information to be sent with the request through React, but I did figure out a workaround.
Basically what I had to do:
In my controller, encrypt the ID of the user and pass it to javascript as a variable.
Pass the encrypted ID variable through the Echo request as part of the header.
Modify the retrieveUser() function to use find(Crypt::decrypt($id)) to lookup the user instead of ->user() (or where() which was strangely not allowed).
From what I can tell, this seems like a decent strategy from a security perspective, but perhaps readers could point out if that is actually correct.
To hack your way into a private channel, you would have to know the ID of the channel you want to listen to, then pass it as an encrypted variable in the header of the request.
Maybe a potential hacker could say that he/she wants to listen to private channel 'brand.1' and all they would have to do is encrypt the number 1 and pass it through the header. I guess I don't know how that works enough to know whether that is possible.
Anyway my goals now are:
converting this into an override setup instead of explicitly changing the Laravel source code.
figuring out if passing the encrypted ID through the request header is secure enough for production.
It does seem like the encrypted ID in the header (which does change every time you run the request) is more secure than simply passing through an 'api_token' which would be a value stored in the users table and is what most people seem to do.

Validate Laravel Nested Rules in Array

I'm with Laravel and I want to write elegant validation rules :) With this Framework its really easy, but I don't know how to approach this when facing 1:n relationships.
I have two Resources, User and Contact. An User can have multiple Contacts.
So, I want a Form where you can fill all User's fields AND all Contact's information.
To do that, I would like to write a Request like this:
UserRequest:
public function rules()
return [
'name' => 'required|string',
'email' => 'required|email|unique:exists:users,id',
'contacts' => 'array',
'contacts.*' => new ContactRequest() // This is the problem
]
My question is: How can I apply this type of validation? Specifically when using array, how can I make a Modular Validation to apply validations of nested Resources? Or should I develop a ContactRule instead?
Edit:
I want that front end send form like this:
` // POST: users
{
'name': 'UserName',
'email': 'user#mail.com'
'contacts': [
[
'email' => 'contac_1#mail.com',
'contact_type_id => 1
],
[
'email' => 'contac_2#mail.com',
'contact_type_id => 2
],
}
`
Thats all,
Thx!
We have an API with 100's of results in each request or perhaps post/patch.
We still use:
'data.relationships.users.data.*.id' => [
'string',
'unique:api_groups,name,' . ($this->route('group')->id ?? 0),
]
So for you
'contacts.*.email' => 'required|email|unique:exists:users,id'
Works perfectly. It doesn't get more complex or anything.

How to set hashUrl to module's defaultRoute in Yii2 / Angular

I am building a single page web application using Yii2(basic) and Angular 5. My backend is a module rather than a separate Yii application.
$config = [
... codes .....
'modules' => [
'backend' => [
'class' => 'app\modules\backend\Module',
'defaultRoute' => 'admin',
],
]
];
The page navigation is managed from angular side as #route ( hashtag route url) like [root_path]/web/#/user/dashboard (for frontend) and [root_path]/web/backend/#/admin/dashboard (for backend).
So, whenever i navigate to [root_path]/web/backend, i want to automatically redirect the url to [root_path]/web/backend/#/admin/dashboard. For this, i tried changing the default route of backend module as:
$config = [
... codes .....
'modules' => [
'backend' => [
'class' => 'app\modules\backend\Module',
'defaultRoute' => \yii\helpers\Url::to(['/admin','#' => '/admin/dashboard']),
],
]
];
AND
$config = [
... codes .....
'modules' => [
'backend' => [
'class' => 'app\modules\backend\Module',
'defaultRoute' => \yii\helpers\Url::to(['/backend/admin/index','#' => '/admin/dashboard']),
],
]
];
But I got this error :
Can anyone point me out what I am doing wrong?
Thanx in advance.
You cannot use Url::to() on defining configuration. At this point application is not yet initialized, so UrlManager component (which is used by Url helper) does not exist. So you're creating chicken-egg problem - you need Application to initialize Application.
And even if you could use it, there is no much sense in what you're trying to do. defaultRoute is not the same as URL and it has nothing to do with redirections - assigning URL into it will not bring anything good.
Moreover, part of URL after # is not sent to the server, so you never get request for URL /web/backend/#/admin/dashboard. From Yii perspective there is no difference whether user is on /web/backend/#/admin/dashboard or /web/backend/ - it will always be seen as /web/backend/.
If you want such redirection, you should handle it in JavaScript and perform at browser level.

how to configure routes for child controllers in CakePHP 3.x?

Reports has many ReportInstances
ReportInstances belongs to Reports
I would like to have the url /reports/:report-id/instances to point to the action index_by_report_id inside ReportInstancesController.php
How do I configure the routes.php accordingly?
UPDATE:
I tried nested resources as described here:
http://book.cakephp.org/3.0/en/development/routing.html#creating-nested-resource-routes
Here are my routes
$routes->resources('Reports', [
'map' => [
'standard' => [
'action' => 'standard',
'method' => 'GET',
]
]
]);
$routes->resources('Reports', function ($routes) {
$routes->resources('ReportInstances');
});
When I do a /reports/1/instances, it goes to ReportsController looking for action 1.
Please advise.
Do this in your routes.php
$routes->resources('Parents', function ($routes) {
$routes->resources('Children');
});
$routes->resources('Children');
In ChildrenController.php,
protected function _prepareConditions() {
$parentId = isset($this->request->params['parent_id']) ? $this->request->params['parent_id'] : null;
if ($parentId == null) {
return [];
}
return [
'Children.parent_id' => $parentId
];
}
public function index()
{
$conditions = $this->_prepareConditions();
$this->paginate = [
'contain' => ['Parents'],
'conditions' => $conditions
];
// ... and so on
You will be able to do the following:
/parents/1/children
/parents/1/children.json
/children
/children.json
Why this works?
http://book.cakephp.org/3.0/en/development/routing.html#creating-nested-resource-routes
tells us that basically to basically retrieve the parent id from the request params.
What it does not say explicitly is that, the routes will then reuse the basic 5 functions: index, add, view, delete, edit even when you nest them under a parent url.
Why do you still have a separate resources route for the Children?
This allows the /children and /children.json to work if you need them as well.
What about add?
I haven't tried that but I do not foresee any issues with using that as
/parents/1/children/add
/parents/1/children/add.json
/children/add
/children/add.json

Resources