Symfony override configuration from the app/config.yml - symfony

I have in my app/config.yml this:
# Doctrine Configuration
doctrine:
dbal:
driver: %database_driver%
host: %database_host%
port: %database_port%
dbname: %database_name%
user: %database_user%
password: %database_password%
orm:
auto_generate_proxy_classes: %kernel.debug%
auto_mapping: true
Now I create my bundle and I want it to override some of this configuration:
doctrine:
orm:
resolve_target_entities:
Acme\UserBundle\Interfaces\User: Acme\MyBundle\Entity\User
I want to add this configuration without changing app/config.yml

I found solution by adding compiler pass:
class OrmResolverCompilerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../../Resources/config'));
$loader->load('orm.yml');
$def = $container->findDefinition('doctrine.orm.listeners.resolve_target_entity');
foreach ($container->getParameter('resolve_target_entities') as $name => $implementation) {
$def->addMethodCall('addResolveTargetEntity', array( $name, $implementation, array() ));
}
$def->addTag('doctrine.event_listener', array('event' => 'loadClassMetadata'));
}
}
And we also need to create orm.yml in the Resources/config directory inside our Bundle.
And finally register compiler pass inside controllerBundle class:
class MyAcmeBundle extends Bundle
{
public function build(ContainerBuilder $container)
{
parent::build($container);
$container->addCompilerPass(new OrmResolverCompilerPass());
}
}

You can now use PrependExtensionInterface for this:
class ThirisCartLogicCatalogExtension extends Extension implements PrependExtensionInterface
{
public function prepend(ContainerBuilder $container)
{
$config = Yaml::parse(file_get_contents(__DIR__.'/../Resources/config/config.yml'));
foreach ($config as $key => $configuration) {
$container->prependExtensionConfig($key, $configuration);
}
}
}
With this code config.yml from your bundle will be unconditionally merged into global confuguration.
Please note, that configuration file you load is not validated for syntax this way.

Related

API platform and Symfony, multiple entity managers and dynamic connection problem on some api operations

The following code shows a solution that almost everything works fine. But not everything ...
I am trying to use separate mysql databases for clients.
This is how the databases structure should look:
SystemEntityManager:
database_system
AccountEntityManager:
database_account_1
database_account_2
database_account_3
database_account_4
...and more and more...
When logged user (with proper JWT token) request API Platform I use a mechanism that selects the appropriate database.
It works during the create, delete and get collection operations.
But... this does NOT work during the get item or update operation
The cause of this problem is wrong database selection.
During these operations an attempt is made to download data from the database_system instead of database_account_1 etc
Problem appear when I request from swagger documentation and functional tests.
This is my doctrine.yaml with configuration for two entity manager:
system
account (this is dynamic connection with multiple client databases)
doctrine:
dbal:
default_connection: system
connections:
system:
url: '%env(resolve:DATABASE_URL)%'
account:
url: '%env(resolve:DATABASE_URL)%'
wrapper_class: App\Doctrine\DynamicConnection
orm:
default_entity_manager: system
entity_managers:
system:
connection: system
naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
mappings:
System:
is_bundle: false
type: annotation
dir: '%kernel.project_dir%/src/Entity/System'
prefix: 'App\Entity\System'
alias: System
account:
connection: account
naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
mappings:
Account:
is_bundle: false
type: annotation
dir: '%kernel.project_dir%/src/Entity/Account'
prefix: 'App\Entity\Account'
alias: Account`
Class to switch connection:
declare(strict_types=1);
namespace App\Doctrine;
use Doctrine\Common\EventManager;
use Doctrine\DBAL\Configuration;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Driver;
final class DynamicConnection extends Connection
{
public function __construct(
array $params,
Driver $driver,
?Configuration $config = null,
?EventManager $eventManager = null
) {
parent::__construct($params, $driver, $config, $eventManager);
}
public function switchConnection(int $account): void
{
if ($this->isConnected()) {
$this->close();
}
$params = $this->getParams();
$params['dbname'] = AccountDatabaseHelper::getAccountDatabaseName($account); // return account_1 or account_2
parent::__construct($params, $this->_driver, $this->_config, $this->_eventManager);
}
}
When request come from API and User is logged then I switch database (I am use user->id as prefix of database )
namespace App\Doctrine;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\ControllerEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
class AccountContextControllerEventSubscriber implements EventSubscriberInterface
{
private AccountContextService $accountContextService;
private TokenStorageInterface $tokenStorage;
public function __construct(AccountContextService $accountContextService, TokenStorageInterface $tokenStorage)
{
$this->accountContextService = $accountContextService;
$this->tokenStorage = $tokenStorage;
}
public function onKernelController(ControllerEvent $event)
{
if (!$event->isMainRequest()) {
return;
}
if ($this->tokenStorage->getToken()) {
$this->accountContextService->switchAccount($this->tokenStorage->getToken()->getUser()->getId());
}
}
public static function getSubscribedEvents()
{
return [
KernelEvents::CONTROLLER => "onKernelController"
];
}
}
This is my service with EventDispatcherInterface (this service is used above code)
namespace App\Doctrine;
use Psr\EventDispatcher\EventDispatcherInterface;
class AccountContextService
{
private EventDispatcherInterface $dispatcher;
public function __construct(EventDispatcherInterface $dispatcher)
{
$this->dispatcher = $dispatcher;
}
public function switchAccount(int $account)
{
$this->dispatcher->dispatch(new AccountContextChangeEvent($account));
}
}
And event subscriber:
namespace App\Doctrine;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class DoctrineAccountContextService implements EventSubscriberInterface
{
private EntityManagerInterface $entityManager;
public function __construct(EntityManagerInterface $accountEntityManager)
{
$this->entityManager = $accountEntityManager;
}
public function onAccountContextChange(AccountContextChangeEvent $event): void
{
$this->switchAccount($event->getAccount());
}
public function switchAccount(int $account): void
{
$connection = $this->entityManager->getConnection();
if ($connection instanceof DynamicConnection) {
$connection->switchConnection($account);
} else {
throw new \LogicException("To switch connection to selected account, connection object must be instance of " . DynamicConnection::class);
}
}
public static function getSubscribedEvents()
{
return [
AccountContextChangeEvent::class => "onAccountContextChange"
];
}
}

Get User inside a SQLFilter in Symfony 3

I’m starting with Symfony and I want to make a multi tenant application.
I want to automatically filter in my SQL queries the content according to the company of belonging of the connected user, every time a table has a link with my company table.
I found the way to create filters but I can not find a way to retrieve in this filter the information about the company of the connected user.
I use FOSuser I override it with my own User class.
my config.yml
#app\config\config.yml
doctrine:
dbal:
...
orm:
auto_generate_proxy_classes: '%kernel.debug%'
naming_strategy: doctrine.orm.naming_strategy.underscore
auto_mapping: true
filters:
company:
class: 'Acme\CompanyBundle\Repository\Filters\CompanyFilter'
enabled: true
my Filter
<?php
#src\Acme\CompanyBundle\Repository\Filters\CompanyFilter.php
namespace Acme\CompanyBundle\Repository\Filters;
use Doctrine\ORM\Mapping\ClassMetaData;
use Doctrine\ORM\Query\Filter\SQLFilter;
use Acme\UserBundle\Entity\UserEntity;
use Acme\CompanyBundle\Entity\CompanyEntity;
class CompanyFilter extends SQLFilter
{
public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias)
{
if ($targetEntity->hasAssociation("company")) {
// here how to get the connected user ???
$company = $user->getCompany();
$idCompany = $company->getId();
return $targetTableAlias . ".company_id = '".$idCompany."'";
}
return "";
}
}
in advance thank you for your help
Set an onKernelRequest listener, pass it the token storage service, so it defines your user as parameter of your SQLFilter.
So in your services.yml add :
services:
on_request_listener:
class: Acme\CompanyBundle\EventListener\OnRequestListener
arguments: ["#doctrine.orm.entity_manager", "#security.token_storage"]
tags:
-
name: kernel.event_listener
event: kernel.request
method: onKernelRequest
Create the listener :
class OnRequestListener
{
protected $em;
protected $tokenStorage;
public function __construct($em, $tokenStorage)
{
$this->em = $em;
$this->tokenStorage = $tokenStorage;
}
public function onKernelRequest(GetResponseEvent $event)
{
if($this->tokenStorage->getToken()) {
$user = $this->tokenStorage->getToken()->getUser();
$filter = $this->em->getFilters()->enable('company');
$filter->setParameter('user', $user);
}
}
}
Then at last your SQLFilter :
<?php
#src\Acme\CompanyBundle\Repository\Filters\CompanyFilter.php
namespace Acme\CompanyBundle\Repository\Filters;
use Doctrine\ORM\Mapping\ClassMetaData;
use Doctrine\ORM\Query\Filter\SQLFilter;
use Acme\UserBundle\Entity\UserEntity;
use Acme\CompanyBundle\Entity\CompanyEntity;
class CompanyFilter extends SQLFilter
{
public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias)
{
if ($targetEntity->hasAssociation("company") && $this->hasParameter('user')) {
$user = $this->getParameter('user');
$company = $user->getCompany();
$idCompany = $company->getId();
return $targetTableAlias . ".company_id = '".$idCompany."'";
}
return "";
}
}

Validation doesn't work using Symfony2.8

I am a newbie in using Symfony2.8 and I got a validation problem.
This is how my code from dependency injection looks like --
<?php
namespace AppBundle\DependencyInjection;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
class AppExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container) {
$r = new \ReflectionClass(get_class($this));
$dir = dirname($r->getFileName());
$loader = new YamlFileLoader($container, new FileLocator($dir.'/../Resources/config'));
$loader->load('validation.yml');
}
}
And then I am trying to validate that those two columns are unique. (UniqueConstraint).
# src/AppBundle/Resources/config/validation.yml
AppBundle\Entity\StaticContent:
constraints:
- Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity:
fields: [build, name]
With StaticContent in the Entity and it complains that it can't find the StaticContent.
nebo#localhost:/var/www/html/iclei$ php app/console doctrine:schema:update --dump-sql
[Symfony\Component\DependencyInjection\Exception\InvalidArgumentException]
There is no extension able to load the configuration for "AppBundle\Entity\StaticContent" (in /var/www/html/iclei/src/AppBundle/DependencyInjection/../Resources/config/validation.yml). Looked for nam
espace "AppBundle\Entity\StaticContent", found none
What am I doing wrong ?
Also I have inside the
app/config/config.yml
framework:
#esi: ~
#translator: { fallbacks: ['%locale%'] }
secret: '%secret%'
router:
resource: '%kernel.root_dir%/config/routing.yml'
strict_requirements: ~
form: ~
csrf_protection: ~
validation: { enabled: true, enable_annotations: true }
And this is the structure of the Bundle which we are building.
DIR/src/AppBundle/Resources/config/doctrine/StaticContent.orm.yml
DIR/src/AppBundle/DependencyInjection/AppExtension.php
This is my doctrine configuration:
# Doctrine Configuration
doctrine:
dbal:
driver: pdo_mysql
host: '%database_host%'
port: '%database_port%'
dbname: '%database_name%'
user: '%database_user%'
password: '%database_password%'
charset: UTF8
# if using pdo_sqlite as your database driver:
# 1. add the path in parameters.yml
# e.g. database_path: '%kernel.root_dir%/data/data.db3'
# 2. Uncomment database_path in parameters.yml.dist
# 3. Uncomment next line:
#path: '%database_path%'
orm:
entity_managers:
default:
mappings:
AppBundle: ~
auto_generate_proxy_classes: '%kernel.debug%'
naming_strategy: doctrine.orm.naming_strategy.underscore
auto_mapping: true
On I13 request I add my entity.
<?php
namespace AppBundle\Entity;
/**
* StaticContent
*/
class StaticContent
{
/**
* #var int
*/
private $id;
/**
* #var int
*/
private $build;
/**
* #var string
*/
private $name;
/**
* #var string
*/
private $type;
/**
* #var string
*/
private $data;
With their getters and setters.
AppBundle\Entity\StaticContent:
type: entity
table: null
repositoryClass: AppBundle\Repository\StaticContentRepository
id:
id:
type: integer
id: true
generator:
strategy: AUTO
fields:
name:
type: string
length: 255
data:
type: blob
manyToOne:
build:
targetEntity: AppBundle\Entity\Build
inversedBy: staticContents
joinColumn:
name: build_id
referencedColumnName: id
lifecycleCallbacks: { }
Please try to enable bundle under doctrine configuration and tell is it works:
# Doctrine Configuration
doctrine:
dbal:
driver: pdo_mysql
host: '%database_host%'
port: '%database_port%'
dbname: '%database_name%'
user: '%database_user%'
password: '%database_password%'
charset: UTF8
orm:
auto_generate_proxy_classes: "%kernel.debug%"
entity_managers:
default:
auto_mapping: true
mappings:
YOURBundle: ~
Change AppExtension:
<?php
namespace AppBundle\DependencyInjection;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\DependencyInjection\Loader;
class AppExtension extends Extension {
/**
* {#inheritdoc}
*/
public function load(array $configs, ContainerBuilder $container) {
$configuration = new Configuration();
$config = $this->processConfiguration($configuration, $configs);
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config'));
}
}

Dynamic database parameters file on Symfony

I'm new to Symfony and i'm trying to load different parameters.yml depending on host, including database configuration. So, in apache vhosts, I defined a variable that let me know what client is in PHP by $_SERVER["CLIENTID"] ( Directive SetEnv ). And now I want to, for example if CLIENTID is "client1", load parameters_client1.yml, that contains db_host, db_user, etc...
I tried this way :
app/config/config.yml
doctrine:
dbal:
host: "%db_host%"
dbname: "%db_name%"
user: "%db_user%"
password: "%db_password%"
app/config/parameters_client1.yml
parameters:
db_host: "localhost"
db_name: "client1_database"
db_user: "client1"
db_password: "something"
src/AppBundle/DependencyInjection/Configuration.php
namespace AppBundle\DependencyInjection;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
class Configuration implements ConfigurationInterface
{
public function getConfigTreeBuilder()
{
$treeBuilder = new TreeBuilder();
$rootNode = $treeBuilder->root('appbundle');
return $treeBuilder;
}
}
src/AppBundle/DependencyInjection/AppExtension.php
namespace AppBundle\DependencyInjection;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
class AppExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container)
{
$configuration = new Configuration();
$config = $this->processConfiguration($configuration, $configs);
$rootdir = $container->getParameter('kernel.root_dir');
// Load the bundle's services.yml
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
$loader->load('services.yml');
// Load parameters depending on current host
$paramLoader = new Loader\YamlFileLoader($container, new FileLocator($rootdir.'/config')); // Access the root config directory
$parameters = "parameters.yml";
if( array_key_exists("CLIENTID", $_SERVER) )
{
$paramfile = sprintf('parameters_%s.yml', $_SERVER["CLIENTID"]);
if (file_exists($rootdir.'/config/'.$paramfile))
{
$parameters = $paramfile;
}
}
$paramLoader->load($parameters);
}
}
But that's not working, I got the following error :
ParameterNotFoundException in ParameterBag.php line 100:
You have requested a non-existent parameter "db_host".
Please can somebody explain me what I've missed ?
I'm using Symfony 3.2
A rough guess are you including the resource in app/config/config.yml
imports:
- { resource: parameters.yml }
- { resource: security.yml }
- { resource: services.yml }

Symfony 2 : multiple and dynamic database connection

I am quite new to SF2 and I was wondering how I could manage connections to severals databases into ONE bundle.
For the moment I have this solution - which works fine - but I don't know if it is the right way to do it....
in myBundle\Ressource\config\config.yml :
doctrine:
dbal:
default_connection: default
connections:
default:
dbname: SERVER
user: root
password: null
host: localhost
client:
dbname: CLIENT_134
user: root
password: null
host: localhost
orm:
default_entity_manager: default
entity_managers:
default:
connection: default
mappings:
MyBundle: ~
client:
connection: client
mappings:
MyBundle: ~
And then, in order to switch to one of the BD or the other, I do :
$O_ressource= $this->get('doctrine')->getEntityManager('client');
$O_ressource= $this->get('doctrine')->getEntityManager('default');
So guys, do you think it is a good way to manage this?
And my second question is :
how to set up dynamic database connection?
I mean I have 100 databases in my system and I can't set all them in my config.yml file.
So I would like to be able to change database on the fly.
Thanks for the help!
If you use ConnectionFactory, your event subscribers attached to the connection will stop working, for example stofDoctrineExtensions.
Here is my method. I have as with ConnectionFactory have empty connection and EntityManager. While working I just replace connection configuration by Reflections. Works on SF 2.0.10 ;)
class YourService extends ContainerAware
{
public function switchDatabase($dbName, $dbUser, $dbPass)
{
$connection = $this->container->get(sprintf('doctrine.dbal.%s_connection', 'dynamic_conn'));
$connection->close();
$refConn = new \ReflectionObject($connection);
$refParams = $refConn->getProperty('_params');
$refParams->setAccessible('public'); //we have to change it for a moment
$params = $refParams->getValue($connection);
$params['dbname'] = $dbName;
$params['user'] = $dbUser;
$params['password'] = $dbPass;
$refParams->setAccessible('private');
$refParams->setValue($connection, $params);
$this->container->get('doctrine')->resetEntityManager('dynamic_manager'); // for sure (unless you like broken transactions)
}
}
UPDATE:
More elegant solution for doctrine 2.2 / sf 2.3 (without relection), created for php5.4 (I love new array initializer :D)
We can use doctrine feature called connection wrapper, see http://docs.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/portability.html
This example use session service for temporary storing connection details.
At first we have to create special connection wrapper:
namespace w3des\DoctrineBundle\Connection;
use Doctrine\DBAL\Connection;
use Symfony\Component\HttpFoundation\Session\Session;
use Doctrine\Common\EventManager;
use Doctrine\DBAL\Events;
use Doctrine\DBAL\Event\ConnectionEventArgs;
/*
* #author Dawid zulus Pakula [zulus#w3des.net]
*/
class ConnectionWrapper extends Connection
{
const SESSION_ACTIVE_DYNAMIC_CONN = 'active_dynamic_conn';
/**
* #var Session
*/
private $session;
/**
* #var bool
*/
private $_isConnected = false;
/**
* #param Session $sess
*/
public function setSession(Session $sess)
{
$this->session = $sess;
}
public function forceSwitch($dbName, $dbUser, $dbPassword)
{
if ($this->session->has(self::SESSION_ACTIVE_DYNAMIC_CONN)) {
$current = $this->session->get(self::SESSION_ACTIVE_DYNAMIC_CONN);
if ($current[0] === $dbName) {
return;
}
}
$this->session->set(self::SESSION_ACTIVE_DYNAMIC_CONN, [
$dbName,
$dbUser,
$dbPass
]);
if ($this->isConnected()) {
$this->close();
}
}
/**
* {#inheritDoc}
*/
public function connect()
{
if (! $this->session->has(self::SESSION_ACTIVE_DYNAMIC_CONN)) {
throw new \InvalidArgumentException('You have to inject into valid context first');
}
if ($this->isConnected()) {
return true;
}
$driverOptions = isset($params['driverOptions']) ? $params['driverOptions'] : array();
$params = $this->getParams();
$realParams = $this->session->get(self::SESSION_ACTIVE_DYNAMIC_CONN);
$params['dbname'] = $realParams[0];
$params['user'] = $realParams[1];
$params['password'] = $realParams[2];
$this->_conn = $this->_driver->connect($params, $params['user'], $params['password'], $driverOptions);
if ($this->_eventManager->hasListeners(Events::postConnect)) {
$eventArgs = new ConnectionEventArgs($this);
$this->_eventManager->dispatchEvent(Events::postConnect, $eventArgs);
}
$this->_isConnected = true;
return true;
}
/**
* {#inheritDoc}
*/
public function isConnected()
{
return $this->_isConnected;
}
/**
* {#inheritDoc}
*/
public function close()
{
if ($this->isConnected()) {
parent::close();
$this->_isConnected = false;
}
}
}
Next register it in your doctrine configuration:
…
connections:
dynamic:
driver: %database_driver%
host: %database_host%
port: %database_port%
dbname: 'empty_database'
charset: UTF8
wrapper_class: 'w3des\DoctrineBundle\Connection\ConnectionWrapper'
And our ConnectionWrapper is properly registered. Now session injection.
First create special CompilerPass class:
namespace w3des\DoctrineBundle\DependencyInjection\CompilerPass;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
class ConnectionCompilerPass implements CompilerPassInterface
{
/**
* {#inheritDoc}
*/
public function process(ContainerBuilder $container)
{
$connection = $container
->getDefinition('doctrine.dbal.dynamic_connection')
->addMethodCall('setSession', [
new Reference('session')
]);
}
}
And we record our new compiler class in *Bundle class:
public function build(ContainerBuilder $container)
{
parent::build($container);
$container->addCompilerPass(new ConnectionCompilerPass());
}
And that its all!
Connection will be created on demand, based on session properties.
To switch database, just use:
$this->get('doctrine.dbal.dynamic_connection')->forceSwitch($dbname, $dbuser, $dbpass);
Advantages
No more reflection
Creation on demand
Elegant and powerfull
Disadvantages
You have to manualy cleanup your entity manager, or create special doctrine event for this
Much more code
You can look into Symfony\Bundle\DoctrineBundle\ConnectionFactory, using the container service doctrine.dbal.connection_factory:
$connectionFactory = $this->container->get('doctrine.dbal.connection_factory');
$connection = $connectionFactory->createConnection(array(
'driver' => 'pdo_mysql',
'user' => 'root',
'password' => '',
'host' => 'localhost',
'dbname' => 'foo_database',
));
That's just a quick example, but it should get you started.
I run into the same needing to have different databases with the same schema for each client. Since symfony 2.3, after the deprecation of the method resetEntityManager, i noticed that the code run well without closing the connection and without resetting the (old Entity) Manager.
this is my current working code:
public function switchDatabase($dbName, $dbUser, $dbPass) {
$connection = $this->container->get(sprintf('doctrine.dbal.%s_connection', 'dynamic_conn'));
$refConn = new \ReflectionObject($connection);
$refParams = $refConn->getProperty('_params');
$refParams->setAccessible('public'); //we have to change it for a moment
$params = $refParams->getValue($connection);
$params['dbname'] = $dbName;
$params['user'] = $dbUser;
$params['password'] = $dbPass;
$refParams->setAccessible('private');
$refParams->setValue($connection, $params);
}

Resources