I want to use symfony2+doctrine2 for a new project. I ran into a little issue with postgresql-schemes. In contrast to mysql you can specify in postgres (like other databases) different schemes. Our productiv database has around 200 schemes for example.
I have to set a schema for my current doctrine connection. How can I do that?
I solved this issue a few months ago in another project, that uses doctrine2 only. I did the following:
$em = Doctrine\ORM\EntityManager::create($connectionOptions, $config);
$em->getConnection()->exec('SET SEARCH_PATH TO foobar');
But I dont know where I should do that in symfony2?
you could try to implement and use your own driver_class and pass the search_path in the PDO DriverOptions, e.g. in your symfony config:
# Doctrine Configuration
doctrine:
dbal:
driver: pdo_pgsql
driver_class: YourNamespace\YourBundle\Doctrine\DBAL\Driver\PDOPgSql\Driver
options:
search_path: YOUR_SEARCH_PATH
The driver could look something like this:
namespace YourNamespace\YourBundle\Doctrine\DBAL\Driver\PDOPgSql;
use Doctrine\DBAL\Platforms;
class Driver extends \Doctrine\DBAL\Driver\PDOPgSql\Driver implements \Doctrine\DBAL\Driver
{
public function connect(array $params, $username = null, $password = null, array $driverOptions = array())
{
// ADD SOME ERROR HANDLING WHEN THE SEARCH_PATH IS MISSING...
$searchPath = $driverOptions['search_path'];
unset($driverOptions['search_path']);
$connection = new \Doctrine\DBAL\Driver\PDOConnection(
$this->_constructPdoDsn($params),
$username,
$password,
$driverOptions
);
$connection->exec("SET SEARCH_PATH TO {$searchPath};");
return $connection;
}
/**
* Constructs the Postgres PDO DSN.
*
* #return string The DSN.
*/
protected function _constructPdoDsn(array $params)
{
$dsn = 'pgsql:';
if (isset($params['host']) && $params['host'] != '') {
$dsn .= 'host=' . $params['host'] . ' ';
}
if (isset($params['port']) && $params['port'] != '') {
$dsn .= 'port=' . $params['port'] . ' ';
}
if (isset($params['dbname'])) {
$dsn .= 'dbname=' . $params['dbname'] . ' ';
}
return $dsn;
}
}
You need the _constructPdoDsn method because it isn't defined as protected in \Doctrine\DBAL\Driver\PDOPgSql\Driver. It's bit "hacky" because we are using PDO DriverOptions and i'm not sure if that's a good way - but it seems to work.
Hope this helps.
Best regards,
Patryk
Since Doctrine 2.5 you can specify the schema name in the #Table annotation:
/**
* Clerk
*
* #Table(schema="schema")
*/
class Clerk { }
The only downside is, the symfony console can't do that, you have to specify it by hand.
Related
I need to change an old Symfony 1.4 application so that it's able to connect to mysql via ssl-connection.
I found a lot about this for Symfony >= 2. But unfortunately not for this dusty one.
For validation purposes I already made it work by editing
./apps/frontend/lib/vendor/symfony/lib/plugins/sfDoctrinePlugin/lib/vendor/doctrine/Connection.php
$this->dbh = new PDO($this->options['dsn'], $this->options['username'],
(!$this->options['password'] ? '':$this->options['password']), array(PDO::ATTR_PERSISTENT => true));
to
$this->dbh = new PDO($this->options['dsn'], $this->options['username'],
(!$this->options['password'] ? '':$this->options['password']),
array(PDO::ATTR_PERSISTENT => true,
PDO::MYSQL_ATTR_SSL_KEY => '/etc/my.cnf.d/ssl/client-key.pem',
PDO::MYSQL_ATTR_SSL_CERT => '/etc/my.cnf.d/ssl/client-cert.pem',
PDO::MYSQL_ATTR_SSL_CA => '/etc/my.cnf.d/ssl/ca-cert.pem'));
But I wonder if this ugly hack is actually the only solution?
It took me a while to see that this connection class is already overwritten (apps/frontend/lib...).
So I only had to make these variables configurable. There is an option in databases.yml configuration called attributes (doctrine::param::attributes). If you pass non-string keys you can get them with getAttribute.
So at least it works (it's inside the try area of connect-method).
$sslOptionKeys = array(PDO::MYSQL_ATTR_SSL_KEY, PDO::MYSQL_ATTR_SSL_CERT, PDO::MYSQL_ATTR_SSL_CA);
foreach($sslOptionKeys as $sslOptionKey) {
if(array_key_exists($sslOptionKey, $this->pendingAttributes)) {
$pdoOptions[$sslOptionKey] = $this->getAttribute($sslOptionKey);
}
}
$this->dbh = new PDO($this->options['dsn'], $this->options['username'],
(!$this->options['password'] ? '':$this->options['password']),
$pdoOptions);
In databases.yml you will have to type the following (comments help to understand these numbers)
all:
doctrine:
class: sfDoctrineDatabase
param:
dsn: mysql:host=localhost;dbname=db
username: user
password: pass
encoding: utf8
attributes:
#PDO::MYSQL_ATTR_SSL_KEY
1010: /etc/my.cnf.d/ssl/client-key.pem
#PDO::MYSQL_ATTR_SSL_CERT
1011: /etc/my.cnf.d/ssl/client-cert.pem
#PDO::MYSQL_ATTR_SSL_CA
1012: /etc/my.cnf.d/ssl/ca-cert.pem
We found that the attributes array was not working. We had to add an event listener that listened for the doctrine 'doctrine.configure_connection' event and set the properties on the connection directly.
class ProjectConfiguration extends sfProjectConfiguration
{
public function setup()
{
//existing code
$this->dispatcher->connect('doctrine.configure_connection', array(
'ProjectConfiguration','addConnectionSSL'
));
}
static public function addConnectionSSL(sfEvent $event){
$connection = $event->getParameters()['connection'];
/* #var $connection Doctrine_Manager */
$other = $connection->getOption('other');
if(!is_array($other)) $other=array();
$other[PDO::MYSQL_ATTR_SSL_CA] = "PATH_TO_CERT_FILE"; //Set this to actual path. You can also set other properties in the same way.
$connection->setOption('other',$other);
}
}
I own an mssql database server, and connect to it using doctrine2(sqlsrv)
I would like to create the new entity instances with a given id. But if I try it, I get an error:
Cannot insert explicit value for identity column in table 'my_test_table' when IDENTITY_INSERT is set to OFF
I've removed the #GeneratedValue annotation. But I still get this error.
After that, I've run this script in the `SQL Server management studio:
SET IDENTITY_INSERT my_test_table ON
Unfortunately I still get the error, and I can't understand why
It has to be called on the doctrine's connection
$em->getConnection()->prepare("SET IDENTITY_INSERT my_test_table ON")->execute();
Something may be different with my setup, or something in Doctrine may have changed, but this wouldn't work for me with Doctrine ORM 2.5.6, PHP 7.0.17, and SQL Server 2014.
Despite setting it before my flush, it wouldn't work. It also couldn't work for multiple tables from a class hierarchy as IDENTITY_INSERT can be on for only one table at a time.
I was able to figure out how to do this by using a wrapper class for the connection. Doctrine supports this with the wrapperClass configuration parameter. Below is my code that worked.
<?php
declare(strict_types=1);
namespace Application\Db;
/**
* Class SqlSrvIdentityInsertConnection
* This class is to enable Identity Insert when using Doctrine with SQLServer.
* Must use this class with the "wrapperClass" configuration option
* for EntityManager::create
*/
class SqlSrvIdentityInsertConnection extends \Doctrine\DBAL\Connection
{
private $tables = [];
private $enabled = [];
public function enableIdentityInsertFor(string $tableName)
{
$this->tables[] = $tableName;
$this->enabled[$tableName] = false;
}
private function setIdentityInsert(string $statement) {
// Must turn off IDENTITY_INSERT if it was enabled, and this table
// isn't in the query. Must do this first!
foreach($this->tables as $tableName) {
if (stristr($statement, "INSERT INTO $tableName") === false) {
if ($this->enabled[$tableName]) {
parent::exec("SET IDENTITY_INSERT " . $tableName . " OFF");
$this->enabled[$tableName] = false;
}
}
}
foreach($this->tables as $tableName) {
if (stristr($statement, "INSERT INTO $tableName") !== false) {
parent::exec("SET IDENTITY_INSERT ".$tableName." ON");
$this->enabled[$tableName] = true;
// Only one can be enabled at a time
return;
}
}
}
public function prepare($statement)
{
$this->setIdentityInsert($statement);
return parent::prepare($statement);
}
}
Here is how it is used when you want to insert some entities with
$em->persist($newEntity);
/** #var SqlSrvIdentityInsertConnection $conn */
$conn = $em->getConnection();
$metadata = $this->session->getClassMetaData(MyEntityClass::class);
$metadata->setIdGeneratorType(ClassMetadata::GENERATOR_TYPE_NONE);
$conn->enableIdentityInsertFor($metadata->getTableName());
$em->flush();
I'm learning MVC in PHP and using PDO for database access.
My database class is as follows (I use DEFINE in a config file for database variables):
class Database extends PDO {
public function __construct($dbconn ="mysql") {
$options = array(PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_OBJ, PDO::ATTR_ERRMODE => PDO::ERRMODE_WARNING);
switch($dbconn) {
case 'usados':
parent::__construct(DB_TYPE_USADOS . ':host=' . DB_HOST_USADOS . ';dbname=' . DB_NAME_USADOS, DB_USER_USADOS, DB_PASS_USADOS, $options);
break;
case 'autos':
parent::__construct(DB_TYPE_AUTOS . ':host=' . DB_HOST_AUTOS . ';dbname=' . DB_NAME_AUTOS, DB_USER_AUTOS, DB_PASS_AUTOS, $options);
break;
case 'servicos':
parent::__construct(DB_TYPE_SERVICOS . ':host=' . DB_HOST_SERVICOS . ';dbname=' . DB_NAME_SERVICOS, DB_USER_SERVICOS, DB_PASS_SERVICOS, $options);
break;
default:
parent::__construct(DB_TYPE_MYSQL . ':host=' . DB_HOST_MYSQL . ';dbname=' . DB_NAME_MYSQL, DB_USER_MYSQL, DB_PASS_MYSQL, $options);
break;
}
}
}
And in an example model, I have:
class Note_Model extends Model {
public $errors = array();
public function __construct() {
parent::__construct($dbconn="mysql");
}
public function getAllNotes() {
$sth = $this->db->prepare("SELECT user_id, note_id, note_text
FROM note
WHERE user_id = :user_id ;");
$sth->execute(array(':user_id' => $_SESSION['user_id']));
return $sth->fetchAll();
}
}
I have 2 questions:
In my database class, is my approach for different database connections (to be used by different models) ok? Is the switch structure well used for this situation and a variable with where to connect?
I can connect to different databases in different models, but how would I get data from different databases in 1 same model if I had the need. Say for example, in a dashboard that shows information from different sources. It's not just $this->db, is it?
Thank you in advanced.
A switch statement in a constructor is a code smell indicating the class is doing too much. I'd create 4 classes (extending PDO or your own base class) and use them instead.
Regarding models, I'm no expert in MVC, but I know you can join tables in different databases provided they're on the same server. However, that could lead to a 'boss' class that breaks the one-database-per-class rule. The best way is probably to have another class that ask whatever models you need for data, and then pieces it together.
I am trying to run my unit test and create a database during setup. For some reason I am getting the error Unknown database 'coretest'. If I create the database though manually and run the test then I get Can't create database 'coretest'; database exists.
The drop database statement works just now the create database.
Here is my setUP and tearDown methods:
class TestCase extends Illuminate\Foundation\Testing\TestCase {
/**
* Default preparation for each test
*/
public function setUp() {
parent::setUp();
DB::statement('create database coretest;');
Artisan::call('migrate');
$this->seed();
Mail::pretend(true);
}
public function tearDown() {
parent::tearDown();
DB::statement('drop database coretest;');
}
}
The reason why you get this error is simply because laravel tries to connect to database specified in config, which doesn't exist.
The solution is to build your own PDO connection from the settings without specifying database (PDO allows this) and run CREATE DATABASE $dbname statement using it.
We used this approach for testing in our project without any problem.
Here some code:
<?php
/**
* Bootstrap file for (re)creating database before running tests
*
* You only need to put this file in "bootstrap" directory of the project
* and change "bootstrap" phpunit parameter within "phpunit.xml"
* from "bootstrap/autoload.php" to "bootstap/testing.php"
*/
$testEnvironment = 'testing';
$config = require("app/config/{$testEnvironment}/database.php");
extract($config['connections'][$config['default']]);
$connection = new PDO("{$driver}:user={$username} password={$password}");
$connection->query("DROP DATABASE IF EXISTS ".$database);
$connection->query("CREATE DATABASE ".$database);
require_once('app/libraries/helpers.php');
// run migrations for packages
foreach(glob('vendor/*/*', GLOB_ONLYDIR) as $package) {
$packageName = substr($package, 7); // drop "vendor" prefix
passthru("./artisan migrate --package={$packageName} --env={$testEnvironment}");
}
passthru('./artisan migrate --env='.$testEnvironment);
require('autoload.php'); // run laravel's original bootstap file
neoascetic has the best answer because essentially you have to boot laravel's database config file again.
So, a clever hack is to create database again after you have dropped it. No need to touch config/database.
public function setUp() {
parent::setUp();
Artisan::call('migrate');
$this->seed();
Mail::pretend(true);
}
public function tearDown() {
parent::tearDown();
DB::statement('drop database coretest;');
DB::statement('create database coretest;');
}
create it like this:
public function setUp() {
parent::setUp();
$pdo = new PDO(
"mysql:host=localhost",
config('database.connections.mysql.username'),
config('database.connections.mysql.password')
);
$pdo->query('CREATE DATABASE test_database');
// set up the new database as the default
config()->set('database.connections.mysql.database', 'test_database');
}
I feel like I have a much cleaner way of doing this. Just execute the commands normally through the shell.
$host = Config::get('database.connections.mysql.host');
$database = Config::get('database.connections.mysql.database');
$username = Config::get('database.connections.mysql.username');
$password = Config::get('database.connections.mysql.password');
echo shell_exec('mysql -h ' . $host . ' -u ' . $username . ' -p' . $password . ' -e "DROP DATABASE ' . $database . '"');
echo shell_exec('mysql -h ' . $host . ' -u ' . $username . ' -p' . $password . ' -e "CREATE DATABASE ' . $database . '"');
In Laravel 5 it's possible to call migrations internally to the Laravel process which ends up running a good bit quicker than using external commands.
In TestCase::setUp (or earlier), call the migration command with:
$kernel = app('Illuminate\Contracts\Console\Kernel');
$kernel->call('migrate');
Writing my first application in PHP that makes use of classes and objects.
I have a DB class which takes a string to select the appropriate config because there are multiple databases. I started out with a login class but scratched that idea in exchange for a user class so I can do user->isLoggedIn stuff. The user class uses MySQL which stores user and login information, as well as credentials for the second database.
$mysql = new db( 'mysql' );
$user = new user( $mysql );
if( !( $user->isLoggedIn() === true ) )
{
goToPage( 'login.php' );
exit();
}
The second database is Sybase and stores account information. I need the credentials from the user class to get lists and accounts information from here. I think an account class should be next but not sure of the best way to do it. Something like this maybe..
$sybase = new db( 'sybase' );
$account = new account( $sybase );
$account_list = $account->getList( $user->info );
$user->info being an array i guess of credentials and info needed for the account table, or is there a better way to go about this?
Edited to include db class example
config.db.php
$config_array = array();
$config_array[ 'mysql' ][ 'dsn' ] = "mysql:host=localhost;dbname=********;charset=UTF-8";
$config_array[ 'mysql' ][ 'user' ] = "********";
$config_array[ 'mysql' ][ 'pass' ] = "**************";
$config_array[ 'sybase' ][ 'dsn' ] = "odbc:DRIVER={Adaptive Server Anywhere 8.0};***************";
$config_array[ 'sybase' ][ 'user' ] = "**********";
$config_array[ 'sybase' ][ 'pass' ] = "*********";
class.db.php
public function __construct( $type )
{
require 'config.db.php';
$this->type = $type;
foreach( $config_array[ $this->type ] AS $key => $value )
{
$this->$key = $value;
}
try
{
$this->connection = new PDO( $this->dsn, $this->user, $this->pass, $this->options );
}
catch( PDOException $ex )
{
log_action( "db->" . $this->type, $ex->getCode() . ": " . $ex->getMessage() );
$this->error = true;
}
return $this->connection;
}
Does something like the following make sense?
class User()
{
public function getAccountList()
{
$this->Account = new Account( new Database( 'sybase' ) );
return $this->Account->list( $this->userid );
}
}
Also, Accounts have different sections (i.e. History & Notes, Financial Transactions, Documents) that will be different 'tabs' on the page. Should each one of those be a class too?
First up, as a secondary answer on the other answer by Dimitry:
The things you'll gain by having a Singleton pale to the things you lose. You get a global object, which every part of your program can read and modify, but you lost testability, readability, and maintainability.
Because the Singleton object is in fact global, you cannot accurately isolate the single units in your application (which is crucial for Unit Testing), because your function is dependant on other components, which are "magically" inserted into it.
You lose readability because method() may actually need other things to work (for instance, the user object needs a db instance, this makes new user($db) much more readable than new user(), because even without looking at the source code, I can tell what the method/object needs to work.
You lose maintainability because of the reason stated above as well. It's harder to tell which components are inserted via the Singleton than it is to see it in the function's "contract", for that reason, it'll be harder for future you and/or any other developer to read and refactor your code.
As a side note, it's considered good naming convention to name Class names with its first letter upper cased, I'll be using this convention from now on.
Let's start from the top. You have 2 possible states for your Db object, MysqlDb, and SybaseDb. This calls for polymorphism. You have an abstract class Db, and two concrete classes MysqlDb and SybaseDb inheriting from it. The instantiation of the correct Db object is the responsibility of a factory
class DbFactory {
private $db;
/**
* Create a new Db object base on Type, and pass parameters to it.
*
* #param string $type Type of database, Mysql or Sybase.
* #param string $dsn The DSN string corresponding to the type.
* #param string $user User credential
* #param string $pass Password credential
*
* #return Db
*/
public function create($type, $dsn, $user, $pass) {
if (!is_a($this->db, "Db")) {
$type = $type . "Db";
$this->db = new $type($dsn, $user, $pass);
}
return $this->db;
}
}
abstract class Db {
/**
* #param $dsn
* #param $user
* #param $pass
*/
public function __construct($dsn, $user, $pass) {
$this->db = new PDO("$dsn", $user, $pass);
$this->db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$this->db->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
}
}
class MysqlDb extends Db {
/*
* More specific, Mysql only implementation goes here
*/
}
class SybaseDb extends Db {
/*
* More specific, Sybase only implementation goes here
*/
}
Now you should ask yourself, who is responsible for getting the list of accounts? Surely they shouldn't fetch themselves (just as much as it isn't the user's responsibility to fetch its own data from the database). It's the User's responsibility to fetch these accounts, using the SybaseDb connection.
In fact, the User needs the following to work:
Sybase database connection - to fetch Account list. We'll pass the DbFactory instance to it, so that we can easily get the instance if it were already instantiated, and instantiate it if not.
State information (logged in status, user ID, user name, etc).
The User isn't responsible to set this data, it needs it in order to work.
So the User constructor should look like this (assuming "ID" and "name" fields):
User::__construct($id, $name, DbFactory $factory);
The User will have a field $accounts, which would hold an array of Account objects. This array would be populated with a User::getAccounts() method.
I'd make Account the member of User class. "User has an Account", right?
Also, speaking about classes, make sure that your two DBs are singletones, and get them via getInstance(), not __construct().