Overriding doctrine entity manager at runtime - symfony

I'm trying to create a demo system in my Symfony2 project to allow creation of a temporary sqlite demo database for each 'demo' user that logs into a system I'm working on so that each user has access to a default set of data and can modify the data without interfering with other demo users logged into the system.
I've started this by defining a demo connection and an orm entry containing reference to all bundles I want to redirect to the temp db in config.yml then overriding the connection details (more precisely the db path) on login to contain the session id of the user logged in. So in theory, when they login they'll have access to a default set of demo data and when they logout, no changes are persisted and there changes were only visible by them.
$this->doctrine->resetManager('demo');
The problem is, when I try to modify the demo connection to point to a new path, when I call the above step (as mentioned on a few posts I've found scattered around the interwebs) the system dies with an error I'm not familiar with and not sure how to tackle. If somebody could help me understand what's going wrong I should be able to fix it but at the moment I've hit a brick wall :(
Error output: http://i.imgur.com/cllbIyy.png
The process is:
Demo user logs in (in_memory, none database stored)
Login listener overrides 'demo' entity manager, changing the DB location to /tmp/portal_demo_[SESSION_ID].db and populating it with demo data
All following database calls should then use demo database
Here's my code:
Config.yml
doctrine:
dbal:
default_connection: default
connections:
demo:
driver: pdo_sqlite
default:
driver: "%database_driver%"
host: "%database_host%"
port: "%database_port%"
dbname: "%database_name%"
user: "%database_user%"
password: "%database_password%"
charset: UTF8
# Following added to allow importing of existing mysql database. DZH
mapping_types:
enum: string
orm:
default_entity_manager: default
entity_managers:
default:
connection: default
mappings:
SPSettingsBundle: ~
SPUserBundle: ~
SPCompanyBundle: ~
SPVenueBundle: ~
SPAccessBundle: ~
SPHardwareBundle: ~
SPResellerBundle: ~
SPVouchersBundle: ~
SPTokenBundle: ~
SPAdminBundle: ~
SPSocialBundle: ~
demo:
connection: demo
mappings:
SPUserBundle: ~
SPCompanyBundle: ~
SPVenueBundle: ~
SPAccessBundle: ~
SPHardwareBundle: ~
SPResellerBundle: ~
SPVouchersBundle: ~
SPTokenBundle: ~
SPAdminBundle: ~
SPSocialBundle: ~
SPLegacyPortalBundle:
type: "annotation"
dir: "Entity"
SPLegacyPortalBundle:
type: "annotation"
dir: "Entity/Radius"
Listener
<?php
namespace SP\DemoBundle\Listener;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
use Doctrine\DBAL\Connection;
use Doctrine\Bundle\DoctrineBundle\Registry;
use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Component\Console\Input\ArrayInput;
class DemoLoginListener {
private $defaultConnection;
private $doctrine;
private $kernel;
public function __construct(Connection $defaultConnection, Registry $doctrine, KernelInterface $kernel) {
$this->defaultConnection = $defaultConnection;
$this->doctrine = $doctrine;
$this->kernel = $kernel;
}
/**
* Create initial database schema
*/
public function createDbSchema() {
$application = new \Symfony\Bundle\FrameworkBundle\Console\Application($this->kernel);
$application->setAutoExit(false);
// Run initial schema install
$application->run(new ArrayInput(array(
'doctrine:schema:create',
'--em' => 'demo'
)));
}
/**
* Populate database with demo data
*/
public function populateDemoData() {
$query = <<<EOT
INSERT INTO `Company` (`account_manager_id`, `package_id`, `name`, `owner_first_name`, `owner_last_name`, `telephone`, `mobile`, `email`, `venues`, `created_by`, `date_created`) VALUES
(NULL, NULL, 'Demo Company', 'Demo', 'User', NULL, NULL, 'demo#company.com', 'N;', 1, '2013-05-16 15:06:16');
EOT;
$this->defaultConnection->query($query);
}
public function onSecurityInteractiveLogin(InteractiveLoginEvent $event) {
$token = $event->getAuthenticationToken();
$user = $token->getUser();
// Only create demo database with the 'demo' in_memory user logs in
if ('demo' == $user->getUsername()) {
$request = $event->getRequest();
$session = $request->getSession();
if ($session) {
$db_name = sprintf('/tmp/portal_demo_%s.db', $session->getId());
$this->defaultConnection->close();
$reflectionConn = new \ReflectionObject($this->defaultConnection);
$reflectionParams = $reflectionConn->getProperty('_params');
$reflectionParams->setAccessible('public');
$params = $reflectionParams->getValue($this->defaultConnection);
$params['driver'] = 'pdo_sqlite';
$params['dbname'] = $db_name;
$params['path'] = $db_name;
unset($params['host']);
unset($params['port']);
unset($params['password']);
unset($params['user']);
$reflectionParams->setValue($this->defaultConnection, $params);
$reflectionParams->setAccessible('private');
$this->doctrine->resetManager('demo');
// $this->defaultConnection->connect();
}
}
// Create the naked database
// $this->createDbSchema();
// Populate the database with demo data
// $this->populateDemoData();
}
}

Related

Dynamic database connection in Symfony

I have a second entity manager, and i want to get parameters from a database and change the DATABASE_URL to connect to another database.
# config/packages/doctrine.yaml
my_entity:
url: '%DATABASE_URL%'
driver: 'pdo_mysql'
server_version: '5.7'
charset: utf8mb4
I try to add a parameter from the process function from Kernel.php, but it's apparently run only one time.
This can be done by changing the data of the Connection class. You need to create your own Connection class and hook it up to the project.
Let's create the required class Connection
// src/Doctrine/DatabaseConnection.php
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Driver;
class DatabaseConnection extends Connection
{
public function __construct(array $params, Driver $driver, $config, $eventManager )
{
// First, let symfony connect to the default database from env so that a Connection instance appears in order to execute a sql query to get the necessary data
parent::__construct($params, $driver, $config, $eventManager);
// Getting data and changing it
$user_database = $this->executeQuery('SELECT * FROM database WHERE user_id = :user_id',['user_id'=>1])->fetch();
$params['dbname'] = $user_database['dbname'];
$params['user'] = $user_database['user'];
$params['password'] = $user_database['password'];
parent::__construct($params, $driver, $config, $eventManager);
}
}
Note: As you can see in the example, to begin with, we connect to our main database from .env so that we can access the desired database by executing an sql query. After receiving the data, we modify it and call the parent's constructor method again to replace the Connection instance with the database we want.
Initialize DatabaseConnection
# config/packages/doctrine.yaml
doctrine:
dbal:
url: '%env(resolve:DATABASE_URL)%'
wrapper_class: App\Doctrine\DatabaseConnection
Note: Here we have connected our handler using the DBAL wrapperClass option

Persistence of sessions Symfony 3

I would like to "identify" anonymous user when they come on a website page to fill up a form. Indeed, for some stats, we need to know how many connections depending on how far the users are filling up the form.
To identify those users I thought about using the key created by Apache to identify a session after a connection.
I am not sure it is possible to get Apache session information (the key in particuly) in the Symfony application....
So I would use the sessionInterface, new in Symfony 3.
I have no problem injecting this object in my Controllers Action. The thing is that no session is started, so no information to get but some general things on the session Object and this is clear :
protected 'started' => boolean false
if I write :
public function indexAction(SessionInterface $session) {
$session->start();
$session_id = $this->get('session')->getId();
....
}
I can get an identification key (which is probably different from Apache session key) but a new session will be created everytime the user press F5.
Maybe the answer is in some configurations.
I would like to get this special key (Apache or Symfony) for each user connecting to the site and the session should remain the same as the one on Apache, being detroyed if the user close the browser or remains inactive for more than ...(cf. apache and php configuration file).
where should I start the session or could it be started automatically when a user connects ?
Thanks for the help.
Note : Nothing to do with the other post. Better read the question before saying it is the same as another one.
NOTE :
What I tried to do after that.
config.yml :
session:
enabled: true
handler_id: session.handler.native_file
save_path: '%kernel.project_dir%/var/sessions/%kernel.environment%'
name: soda
cookie_lifetime: 0
Controller :
public function indexAction(Request $request, SessionInterface $session)
{
$session_id = null;
$cookies = $request->cookies;
if(!$session->isStarted()) {
print("session not started");
} else {
print("session started");
}
if($cookies->has('soda')) {
print("cookie here");
$session_id = $cookies->get('soda');
} else if(!$session->isStarted()) {
print("cookie not here...starting session");
$session->start();
$session_id = $session->getId();
} else {
print("cookie not here");
}
$response = $this->render('#my.twig', array(
'session_id' => $session_id
));
print_r($session_id);
print_r($session->getId());
$response->headers->setCookie(new Cookie('soda', $session_id));
return $response;
}
First time going on the site :
session not started
cookie not here...starting session
9hec8bd0t7qjr29ji6fuf5 / 9hec8bd0t7qjr29ji6fuf5
I press F5 :
session started
cookie here
9hec8bd0t7qjr29ji6fuf5 / m6alskkqmlf8pt6e1vulj3c8o6
So this time the session is started entering the controller but obviously it has been restarted !!! on every request it seems.
I hope this will help people if it happens to them. I found the solution but I still dont know why it works like this :
config.yml:
framework:
session:
enabled: true
handler_id: session.handler.native_file
save_path: '%kernel.project_dir%/var/sessions/%kernel.environment%'
name: myCookie
cookie_lifetime: 0
in the controller :
public function indexAction(Request $request, SessionInterface $session) {
$cookies = $request->cookies;
$session_id = null;
if($cookies->has('myCookie')) {
$session_id = $cookies->get('myCookie');
} else if (!$session->isStarted() {
$session->start();
$session_id = $session->getId();
$session->set('myCokie', new Cookie('myCookie', $session_id);
}
$response = $this->render ......
$response->headers->setCookie(new Cookie('myCookie', $session_id));
return $response;
}
What about creating a cookie with the session ID generated by your
$session->start();
$session_id = $this->get('session')->getId();
You can then store it in your DB (by creating a Visitor class.
When the page reload, check if a cookie exist and match it with the DB.

Symfony2 - Redirect response from request EventListener in dev mode while ignoring built in request events

I am building my own user management system in Symfony2 (not using FOSUserBundle) and want to be able to force users to change their password.
I have setup an EventListener to listen to the kernal.request event, then I perform some logic in the listener to determine if the user needs to change their password; if they do, then they are redirected to a "Change Password" route.
I add the service to my config.yml to listen on the kernal.request:
password_change_listener:
class: Acme\AdminBundle\EventListener\PasswordChangeListener
arguments: [ #service_container ]
tags:
- { name: kernel.event_listener, event: kernel.request, method: onMustChangepasswordEvent }
And then the listener:
public function onMustChangepasswordEvent(GetResponseEvent $event) {
$securityContext = $this->container->get('security.context');
// if not logged in, no need to change password
if ( !$securityContext->isGranted('IS_AUTHENTICATED_REMEMBERED') )
return;
// If already on the change_password page, no need to change password
$changePasswordRoute = 'change_password';
$_route = $event->getRequest()->get('_route');
if ($changePasswordRoute == $_route)
return;
// Check the user object to see if user needs to change password
$user = $this->getUser();
if (!$user->getMustChangePassword())
return;
// If still here, redirect to the change password page
$url = $this->container->get('router')->generate($changePasswordRoute);
$response = new RedirectResponse($url);
$event->setResponse($response);
}
The problem I am having is that in dev mode, my listener is also redirecting the profiler bar and assetic request events. It works when I dump assets and clear cache and view the site in production mode.
Is there a way I can ignore the events from assetic/profiler bar/any other internal controllers? Or a better way to redirect a user to the change_password page (not only on login success)?
Going crazy thinking up wild hack solutions, but surely there is a way to handle this elegantly in Symfony2?
This is the very hack solution I am using for now:
Determine if in dev environment
If so, get an array of all the routes
Filter the route array so that only the routes I have added remain
Compare the current route to the array of routes
If a match is found, this means that the event is not an in-built controller, but must be one that I have added, so perform the redirect.
And this is the madness that makes that work:
// determine if in dev environment
if (($this->container->getParameter('kernel.environment') == 'dev'))
{
// Get array of all routes that are not built in
// (i.e You have added them yourself in a routing.yml file).
// Then get the current route, and check if it exists in the array
$myAppName = 'Acme';
$routes = $this->getAllNonInternalRoutes($myAppName);
$currentRoute = $event->getRequest()->get('_route');
if(!in_array($currentRoute, $routes))
return;
}
// If still here, success, you have ignored the assetic and
// web profiler actions, and any other actions that you did not add
// yourself in a routing.yml file! Go ahead and redirect!
$url = $this->container->get('router')->generate('change_password_route');
$response = new RedirectResponse($url);
$event->setResponse($response);
And the crazy hack function getAllNonInternalRoutes() that makes it work (which is a modification of code I found here by Qoop:
private function getAllNonInternalRoutes($app_name) {
$router = $this->container->get('router');
$collection = $router->getRouteCollection();
$allRoutes = $collection->all();
$routes = array();
foreach ($allRoutes as $route => $params)
{
$defaults = $params->getDefaults();
if (isset($defaults['_controller']))
{
$controllerAction = explode(':', $defaults['_controller']);
$controller = $controllerAction[0];
if ((strpos($controller, $app_name) === 0))
$routes[]= $route;
}
}
return $routes;
}

how to get the session variable in the view in symfony2

Thanks for your valuable suggestions
i have created a login system where i want to store the id's of users in session variables
this is my controller for login system
use Symfony\Component\HttpFoundation\Session\Session;
class successController extends Controller
{
public function successAction(Request $request)
{
--some code for form--
$repository = $em->getRepository('RepairStoreBundle:users');
$query = $repository->auth($name,$password);
$error="sorry invalid username or password";
if($query== false)
{
return $this->render('RepairLoginBundle:login:login.html.php', array(
'form' => $form->createView(),'error'=>$error,));
}
else
{
$role=$query[0]['role'];
$id=$query[0]['id'];
if($role == 1)
{
$session = new Session();
$session->start();
$session->set('id',$id);
$result=$repository->display();
return $this->render('RepairLoginBundle:login:success.html.php',array('result'=>$result,));
}
else
{
$session = new Session();
$session->start();
$session->set('id',$id);
$res= $repository->edit($id);
return $this->render('RepairLoginBundle:login:user.html.php',array('res'=>$res));
}
}
}
}
when admin logins with role=1 it will render to success.html.php
in this view how can i get the session variable which i have set in the controller.
i have used $session->get('id');
it is giving me server error please help with this
Upfront Authentication should better be done with the Security Component in Symfony2.
Read more about it in The Book - Security. You should probably also take a look at FOSUserBundle
Accessing the session from a PHP template in symfony2:
echo $app->getSession()->get('whatever');
Session Handling
There is an article in the official documentation:
Components/HttpFoundation - Session Data Management
The API documentation for the Session Component can be found here:
http://api.symfony.com/master/Symfony/Component/HttpFoundation/Session/Session.html
In the symfony2 standard-edition you can get the session from within a controller with:
$session = $this->getRequest()->getSession();
As you already have the request as an argument in successAction you could access the session with:
$session = $request->getSession();
Set a value with ( $value needs to be serializable ):
$session->set('key',$value);
Get a value with:
$session->get('key');
Saving (and closing) the session can be done with:
$session->save();
You should also loook at the SessionBag class.
you create a SessionBag and register it with the session. see:
Symfony API
In the registered SessionBag - which implements AttributeBagInterface - you can get and set your key/value's as desired.
TIP: If you want to get the current User and you have a container aware controller ( container injected )
you can use:
$user = $this->container->get('security.context')->getToken()->getUser();
if you are extending Symfony's Controller class in the standard-edition - the shorter way is:
$user = $this->get('security.context')->getToken()->getUser();
or even shorter (Symfony > 2.1.x):
$user = $this->getUser();
Alternative ( If your controller is not container aware ):
Define the controller as a service and inject #security.context:
YAML:
# src/Vendor/YourBundle/Resources/config/services.yml
services:
my.controller.service:
class: Vendor\YourBundle\Controller\successController
arguments: ["#security.context"]
Vendor\YourBundle\Controller\successController:
protected $securityContext;
public function __construct(SecurityContextInterface $securityContext)
{
$this->securityContext = $securityContext;
}
then in your action:
$user = $this->securityContext->getToken()->getUser();
Note:: you have to use the service in your routing aswell if you choose the controller-as-service variant. example routing.yml :
[...]
route_name:
pattern: /success
defaults: { _controller: my.controller.service:successAction }
[...]
[...]
Note... you can also inject the session with "#session"
# src/Vendor/YourBundle/Resources/config/services.yml
[...]
arguments: ["#security.context","#session"]
Note injecting the whole container is resource-heavy. advanced developers inject their needed services one-by-one and not the whole container.
Tip: Normally Controller classes are written with a capital first letter - example: *S*uccessController
General TIP: You have unnecessary dublicate code in your example:
// 'if' and 'else' execute the same stuff here
// result: dublicate code = more code = harder to read
if($role == 1)
{
$session = new Session();
$session->start();
[...]
}
else
{
$session = new Session();
$session->start();
[...]
}
should better be ...
// better: put this stuff before the if/else statement
$session = new Session();
$session->start();
if($role == 1)
{
[...]
}
else
{
[...]
}

Allow switching/impersonating only to particular users in Symfony2

EXPLANATION
Allowing user to switch to any other user is easy in Symfony2. My question is, how to allow certain user to switch only to certain other users? I'm using FOSUserBundle as my users provider.
EXAMPLE
There is 5 users, UserA, UserB, UserC, UserD, UserE, but only 3 of them are linked together.
UserA can switch to UserB and UserC
UserB can switch to UserA and UserC
UserC can switch to UserA and UserB
Thanks for your help!
You could possibly implement this type of thing by overriding the default SwitchUserListener.
The parameter you would override is: security.authentication.switchuser_listener.class
For example, in parameters.yml:
parameters:
security.authentication.switchuser_listener.class: My\Project\Security\Listener\SwitchUserListener
Where, in your custom listener class, you'd implement Symfony\Component\Security\Http\Firewall\ListenerInterface and use the existing Symfony\Component\Security\Http\Firewall\SwitchUserListener class as a basis for your custom implementation.
If you want to impersonate Admin Users in to Regular user there are some examples :)
https://gist.github.com/1589120
http://symfony.com/doc/current/book/security.html#impersonating-a-user
Thanks to Veonik, a (maybe dirty) way to do that
Add a role with that syntax :
ROLE_ALLOWED_TO_SWITCH_{usertype}
For example add ROLE_A to userA :
ROLE_A:
- ROLE_ALLOWED_TO_SWITCH_USERTYPEB
- ROLE_ALLOWED_TO_SWITCH_USERTYPEC
Declare your own SwitchUserListener service in yml :
security.authentication.switchuser_listener:
class: %security.authentication.switchuser_listener.class%
public: false
abstract: true
tags:
- { name: monolog.logger, channel: security}
arguments:
- #security.context
- null
- #security.user_checker
- null
- #security.access.decision_manager
- #logger
- _switch_user
- ROLE_ALLOWED_TO_SWITCH
- #event_dispatcher
- #security.role_hierarchy
In SwitchUserListener::attemptSwitchUser method
private function attemptSwitchUser(Request $request)
{
....
CODE BEFORE
....
$user = $this->provider->loadUserByUsername($username);
$rtoSwith = $this->getRolesToSwitchTo($token->getRoles());
$accessGranted = false;
foreach ($rtoSwith as $suType) {
$instance = ucfirst(strtolower($suType));
if ($user instanceof $instance) {
$accessGranted = true;
break;
}
}
if (!$accessGranted) {
throw new AccessDeniedException('Access Denied, not allowed to switch to type : '.get_class($user));
}
....
CODE AFTER
....
return $token
}
protected function getRolesToSwitchTo($roles){
$rts = array();
foreach ($this->roleHierarchy->getReachableRoles($roles) as $role) {
$tmp = explode("ROLE_ALLOWED_TO_SWITCH_", $role->getRole());
if(isset($tmp[1])){
$rts[] = $tmp[1];
}
}
return $rts;
}
Tell me if you have another solution
++

Resources