Performing migration programmatically in Doctrine 2 - php

Given I have a path to Doctrine migration classes. How could I perform the migration programmatically in Doctrine 2?
I assume there should be a clean way to perform the migration over the API as it could have be done with earlier versions of Doctrine as described here:
http://docs.doctrine-project.org/projects/doctrine1/en/latest/en/manual/migrations.html

As I don't see any answers, I'll try to provide my solution for performing migration programmatically (with code) in doctrine.
use Doctrine\DBAL\DriverManager;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\Configuration\Configuration;
use Doctrine\Migrations\Configuration\Connection\ExistingConnection;
use Doctrine\Migrations\Configuration\Migration\ExistingConfiguration;
use Doctrine\Migrations\DependencyFactory;
use Doctrine\Migrations\Metadata\Storage\TableMetadataStorageConfiguration;
use Doctrine\Migrations\MigratorConfiguration;
use Doctrine\Migrations\Provider\SchemaProvider;
use Doctrine\Migrations\Version\Direction;
// connection
$dbParams = [
'dbname' => 'database-name',
'user' => 'database-username',
'password' => 'database-password',
'host' => 'hostname',
'port' => 'port',
'driver' => 'pdo_mysql',
];
try {
$connection = DriverManager::getConnection($dbParams);
} catch (\Doctrine\DBAL\Exception $e) {
echo 'Problem connecting to DB. '.$e->getMessage();
die();
}
// configuration - Be careful what namespace you use
$configuration = new Configuration($connection);
$configuration->addMigrationsDirectory('MyAppNamespace\Migrations', __DIR__ . '/../migrations');
// we want the execution of the migration to make changes to the table doctrine_migration_versions - so the system is aware that executed the migration
$storageConfiguration = new TableMetadataStorageConfiguration();
$storageConfiguration->setTableName('doctrine_migration_versions');
$configuration->setMetadataStorageConfiguration($storageConfiguration);
$dependencyFactory = DependencyFactory::fromConnection(
new ExistingConfiguration($configuration),
new ExistingConnection($connection))
);
$planCalculator = $dependencyFactory->getMigrationPlanCalculator();
// which migration to execute / I assume latest /
$latestMigrationVersion = $dependencyFactory->getVersionAliasResolver()->resolveVersionAlias('latest');
// check if we have at least one migration version to execute
if(!$latestMigrationVersion->equals(new Version(0))){
try {
// so we will execute only latest ONE migration, if you need more, just find a way to list them in the first parameter of method getPlanForVersions()
$planUp = $planCalculator->getPlanForVersions(
[$latestMigrationVersion],
Direction::UP
);
$dependencyFactory->getMetadataStorage()->ensureInitialized();
// do the migration
$dependencyFactory->getMigrator()->migrate($planUp, (new MigratorConfiguration())->setAllOrNothing(false));
}catch (Exception $e){
echo 'There were problems during db-migration.'."\n".$e->getMessage()."\n\n";
}
}
Hope it helps another developer to quick start his prototype.
I tried to be detailed about the code, so people do not waste time into figuring out every single dependency.

Asuming you are using Symfony's DoctrineMigrationsBundle
To migrate to the latest available version use:doctrine:migrations:migrate command.
Here are more available commands.

Related

Doctrine Exception: Deadlock found when trying to get lock

I have a Symfony app which exposes a collection of JSON web services used by a mobile app.
On the last few days we are having many concurrent users using the app (~5000 accesses per day) and a Doctrine error started to "randomly" appear in my logs. It appears about 2-3 times per day and this is the error:
Uncaught PHP Exception Doctrine\DBAL\Exception\DriverException: "An exception occurred while executing 'UPDATE fos_user_user SET current_crystals = ?, max_crystals = ?, updated_at = ? WHERE id = ?' with params [31, 34, "2017-12-19 09:31:18", 807]:
SQLSTATE[40001]: Serialization failure: 1213 Deadlock found when trying to get lock; try restarting transaction" at /var/www/html/rollinz_cms/releases/98/vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/AbstractMySQLDriver.php line 115
It seems it cannot get the lock while updating the users table. The controller code is the following:
/**
* #Rest\Post("/api/badges/{id}/achieve", name="api_achieve_badge")
*/
public function achieveAction(Badge $badge = null)
{
if (!$badge) {
throw new NotFoundHttpException('Badge not found.');
}
$user = $this->getUser();
$em = $this->getDoctrine()->getManager();
$userBadge = $em->getRepository('AppBundle:UserBadge')->findBy(array(
'user' => $user,
'badge' => $badge,
));
if ($userBadge) {
throw new BadRequestHttpException('Badge already achieved.');
}
$userBadge = new UserBadge();
$userBadge
->setUser($user)
->setBadge($badge)
->setAchievedAt(new \DateTime())
;
$em->persist($userBadge);
// sets the rewards
$user->addCrystals($badge->getCrystals());
$em->flush();
return new ApiResponse(ApiResponse::STATUS_SUCCESS, array(
'current_crystals' => $user->getCurrentCrystals(),
'max_crystals' => $user->getMaxCrystals(),
));
}
I looked into MySQL and Doctrine documentation but I couldn't find a reliable solution. Doctrine suggests retrying the transaction but it doesn't show an actual example:
https://dev.mysql.com/doc/refman/5.7/en/innodb-deadlock-example.html
try {
// process stuff
} catch (\Doctrine\DBAL\Exception\RetryableException $e) {
// retry the processing
}
This posts suggests retrying the transaction. How can I do it?
Could it be a server problem (too many accesses) and I must boost the server or the code is wrong and I must explicitly handle the deadlock in my code?
This is a MySQL issue. Multiple simultaneous transactions blocking the same resources.
Check if you have cronjobs that may block the records for long times.
Otherwise is just concurrent requests updating the same data, you may have better knowledge where this data gets updated.
Dirty attempt for a retry in php:
$retry=0;
while (true) {
try {
// some more code
$em->flush();
return new ApiResponse(ApiResponse::STATUS_SUCCESS, array(
'current_crystals' => $user->getCurrentCrystals(),
'max_crystals' => $user->getMaxCrystals(),
));
} catch (DriverException $e) {
$retry++;
if($retry>3) { throw $e; }
sleep(1); //optional
}
}
Albert's solution is the right one but you also must recreate a new EntityManager in the catch clause using resetManager() of your ManagerRegistry. You'll get exceptions if you continue to use the old EntityManager and its behavior will be unpredictable. Beware of the references to the old EntityManager too.
This issue will be hopefully corrected in Doctrine 3: See issue
Until then, here is my suggestion to handle the problem nicely: Custom EntityManager

Rollback in laravel has no effect

I am creating a backup system but I got an issue with the import step. Impossible to rollback if there is any error.
I'm using this code on the CLI mode (via artisan myowncommand)
I have a file, with all my instructions:
<?php
// #generated 2016-12-11 01:05:25
use Illuminate\Support\Facades\DB;
use App\Category;
// ...
DB::beginTransaction();
Category::truncate();
// ...
// LOAD MODEL `Category` (CLASS `App\Category`)
Category::unguard();
Category::create(array (
'id' => 1,
'name' => 'Holidays',
'descr' => 'Holidays & sick & off',
'color' => 'default',
'created_at' => '2016-12-11 01:05:21',
'updated_at' => '2016-12-11 01:05:21',
));
// ...
Category::reguard();
// ...
DB::rollback();
?>
If I execute this code, it's like if no rollback happened :( If I clean my table and execute the script ... data are inserted.
According the laravel documentation, rollback instruction should be compatible:
Using the DB facade's transaction methods also controls transactions
for the query builder and Eloquent ORM (https://laravel.com/docs/5.3/database).
I also tried to use the DB::transaction() (with manual error (changed id by ida) to trigger rollback). No difference.
Do you have any explanation ? Or any way to execute correctly the rollback instruction ?
Thanks :)
Solutions I tested
Test 1
try {
DB::beginTransaction();
include $file;
/*** File is:
* use Illuminate\Support\Facades\DB;
* use App\Category;
*
* Category::truncate();
* Category::unguard();
* Category::create(array ('id' => 1, 'name' => 'Holidays'));
* // create error by changing 'id' by 'ida'
* Category::reguard();
***/
DB::commit();
echo sprintf('> Load Backup file `%s` COMPLETED', $file);
}
catch( \Exception $e ) {
DB::rollback();
echo sprintf('> Load file `%s` failled - ROLLBACK', $file);
throw $e;
}
Test 2
DB::transaction(function() {
include $file;
/*** File is:
* use Illuminate\Support\Facades\DB;
* use App\Category;
*
* Category::truncate();
* Category::unguard();
* Category::create(array ('id' => 1, 'name' => 'Holidays'));
* // create error by changing 'id' by 'ida'
* Category::reguard();
***/
});
Test 3 & 4
Same as test 1, include $file and all DB::transaction, DB::beginTransaction & DB::commit & DB:rollback directly in this $file file
Test 5
I tried to:
move the DB::beginTransaction outside the try/catch
move the DB::commit outside the try/catch
Test case
php artian migrate:refresh --seed Clear tables & populate with some masterdata
On phpmyadmin, clean (truncable) table (here: category).
There is no result on the table
Execute the code. "Load Backup failled - ROLLBACK" is executed
On phpmyadmin, check data: there are records. Model:truncable from my code executed, but not rollback
I think it should be
DB::beginTransaction();
try
{
//code for processing multiple related transactions
include $file;
}
catch(\Exception $e)
{
DB::rollBack();
//echo error message
}
DB::commit();
//echo success message
DB::beginransaction and DB::commit should be outside the try-catch block. It works for me.
And your $file should have only the instructions which you want within the try block - no DB::beginTransaction(), DB::commit() or DB::rollBack() statements.
UPDATE
The general logical steps to follow would be
Begin transaction - DB::beginTransaction()
Run database queries - crud operations - try{//run queries}
If there's an error/exception - Rollback the transaction - catch(Exception $e){ DB::rollBack()
Send/display the error/exception message - //display/send error/exception message }
If the queries run smoothly - Commit the transaction - DB::commit()
Hope this helps.
I was with the same problem, I start reading the documentation carefully and it was mysql version, must be 5.7+.
some reason a rollback often requires you to run composer dump-autoload. if your migration work.

Doctrine not finding data on Google App Engine?

When I do a simple query, like finding all users, it returns an empty array.
$users = $em->getRepository('MyApp\\Model\\Entity\\User')->findAll();
However, when I connect to my database manually, using PDO, it finds the data. I am using the ArrayCache method, to make sure it has nothing to do with GAE not having a filesystem. The GAE docs say you can use sys_get_temp_dir(), so I don't think it's my proxies. I'm at a loss for why Doctrine is returning nothing and not throwing any errors as well.
Here is my bootstrap file for my app:
<?php
$baseDir = dirname(dirname(__FILE__));
define('TIMEZONE_OFFSET', \MyApp\Library\Date::getMyTimezoneOffset());
use Doctrine\Common\Annotations\AnnotationReader;
use Doctrine\Common\Annotations\AnnotationRegistry;
// globally used cache driver, in production use APC or memcached
$cache = new Doctrine\Common\Cache\ArrayCache;
// standard annotation reader
$annotationReader = new AnnotationReader;
AnnotationReader::addGlobalIgnoredName('dummy');
AnnotationRegistry::registerFile(__DIR__ . "/doctrine/orm/lib/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php");
AnnotationRegistry::registerFile(__DIR__ . "/Gedmo/Timestampable/Mapping/Driver/Annotation.php");
AnnotationRegistry::registerAutoloadNamespace("\\MyApp\\Model\\Entity", $baseDir);
$cachedAnnotationReader = new Doctrine\Common\Annotations\CachedReader(
$annotationReader, // use reader
$cache, // and a cache driver
$debug = LOCAL
);
// create a driver chain for metadata reading
$driverChain = new Doctrine\ORM\Mapping\Driver\DriverChain();
// load superclass metadata mapping only, into driver chain
// also registers Gedmo annotations.NOTE: you can personalize it
Gedmo\DoctrineExtensions::registerAbstractMappingIntoDriverChainORM(
$driverChain, // our metadata driver chain, to hook into
$cachedAnnotationReader // our cached annotation reader
);
// now we want to register our application entities,
// for that we need another metadata driver used for Entity namespace
$annotationDriver = new Doctrine\ORM\Mapping\Driver\AnnotationDriver(
$cachedAnnotationReader, // our cached annotation reader
array(ENTITY_PATH) // paths to look in
);
// NOTE: driver for application Entity can be different, Yaml, Xml or whatever
// register annotation driver for our application Entity namespace
$driverChain->addDriver($annotationDriver, 'MyApp\\Model\\Entity');
// general ORM configuration
$config = new Doctrine\ORM\Configuration;
$config->setProxyDir(sys_get_temp_dir());
$config->setProxyNamespace('Proxy');
$config->setAutoGenerateProxyClasses(Doctrine\Common\Proxy\AbstractProxyFactory::AUTOGENERATE_FILE_NOT_EXISTS); // this can be based on production config.
// register metadata driver
$config->setMetadataDriverImpl($driverChain);
// use our already initialized cache driver
$config->setMetadataCacheImpl($cache);
$config->setQueryCacheImpl($cache);
// create event manager and hook preferred extension listeners
$evm = new Doctrine\Common\EventManager();
// gedmo extension listeners, remove which are not used
// timestampable
$timestampableListener = new Gedmo\Timestampable\TimestampableListener;
$timestampableListener->setAnnotationReader($cachedAnnotationReader);
$evm->addEventSubscriber($timestampableListener);
// mysql set names UTF-8 if required
$evm->addEventSubscriber(new Doctrine\DBAL\Event\Listeners\MysqlSessionInit());
$dbParams = array(
'driver' => 'pdo_mysql',
'user' => DB_USER,
'password' => DB_PASSWORD,
'dbname' => DB_NAME,
'host' => DB_HOST,
'port' => DB_PORT,
'unix_socket' => DB_UNIX_SOCKET
);
// Finally, create entity manager
$em = Doctrine\ORM\EntityManager::create($dbParams, $config, $evm);
Update
Just for clarity:
This returns an empty array:
$users = $em->getRepository('MyApp\\Model\\Entity\\User')->findAll();
\Doctrine\Common\Util\Debug::dump($users);
And this returns an array with users in it. So confused.
$pdo = $em->getConnection();
$users = $pdo->query('SELECT * FROM user');
var_dump($users->fetchAll());
My issue was that I didn't create a Company in my database and my User entity requires a Company, so Doctrine used an INNER JOIN and thus, no users. Ugh.
Update
See this question: Why does Doctrine2 do an INNER JOIN for findAll()?

exception: can't temprelease nested lock

I am trying to run some server-side JS in Mongo. Operations I am trying to perform are:
db.dropDatabase(); // removing current database
db.copyDatabase('db_dump', 'db', 'localhost'); // substituting it with a dump
Everything works perfectly nice. When I am storing this as a function:
function () {
db.dropDatabase();
return db.copyDatabase('db_dump', 'db', 'localhost');
}
and executing it, everything is also nice and returns me {"ok" : 1}
But when I try to execute this using php driver:
$db->execute("function(){ db.dropDatabase(); return db.copyDatabase('db_dump', 'db', 'localhost'); }");
I get
{
"retval": {
"errmsg":"exception: can't temprelease nested lock",
"code":10298,
"ok":0
},
"ok":1
}
My first though was that I just need to get out of the lock, so I tried this
$db->command(
array(
'$eval' => "function() { db.dropDatabase(); return db.copyDatabase('db_dump', 'db', 'localhost');}"
),
array(
'nolock'=> true
)
);
Nothing else is using database at this point.
Any ideas how to get rid of this error?
I am using Mongo 2.4.4, PHP 5.3.13 and driver 1.2.10
P.S. Tried this on PHP 5.4.16 and the situation is the same
This command cannot be invoked through the eval command (see this thread for some additional discussion on the matter). Rather than use JS methods, you can invoke the copydb command directly with MongoDB::command(). The following script will copy between two databases on the same server, since I omitted the fromhost option:
$m = new MongoClient();
$m->a->drop();
$m->admin->command([
'copydb' => 1,
'fromdb' => 'b',
'todb' => 'a',
]);

What are the step you have to go through when updating an application to be multilingual?

I need to update an application which is built on Zend Framework.
Most of the text is hard-coded in views scripts, forms, etc.
The application will be available in say, 3 languages AND is language specific (the content is not the same for all) and will have one domain per language (ie: mygreatsite.com, monsupersite.com, ilmiosupersite.com, etc.)
First question:
What is the best way to "handle" this kind of application?
I can imagine several solution like:
One copy per language, using different db, etc... (probably not the best way for maintenance)
Only one application, handling different content, db, etc, depending on the locale (based on the route)
Second question:
What should I need to know about the existing code to start the "migration"?
What about any best practice when building a i18n website?
What are the best adapter? (I already used gettext() and I think it's the best)
I am by no means an expert but this is what I do.
I use array as my translation adapter because it’s easier for my clients to update as they are just regular Joes. And I use translation keys instead of sentences. For example
Some people would use
$this->translate(‘Some sentence to translate’);
I use
$this->translate(‘default-index-dashboard-title’);
This makes it far easier for me to know where the text I’m looking for is to change. I don’t know if there are any advantages other than that though.
You will need to setup your translation adapter and translation cache (if you want) in your bootstrap. I do mine like this:
protected function _initLocale()
{
$locale = new Zend_Locale(Zend_Locale::BROWSER);
$config = Zend_Registry::get('config');
Zend_Registry::set('Zend_Locale', $locale->toString());
return $locale;
}
protected function _initTranslation()
{
try{
$translate = new Zend_Translate(array('adapter' => 'array', 'content' => ROOT . '/callmanagement/languages/' . strtolower(Zend_Registry::get('Zend_Locale')) . '.php'));
}catch(Exception $e){
$translate = new Zend_Translate(array('adapter' => 'array', 'content' => ROOT . '/callmanagement/languages/en_gb.php'));
}
Zend_Registry::set('Zend_Translate', $translate);
return $translate;
}
I would use a single code base unless the sites are completely different and store the shared data in one database and have other databases for the site specific stuff.
You can setup multiple db adapters either in the bootstrap or in the congfig.
$dbLocal = new Zend_Db_Adapter_Pdo_Mysql(array(
'host' => 'localhost',
'username' => $result['user'],
'password' => $result['password'],
'dbname' => $result['database']
));
Zend_Db_Table_Abstract::setDefaultAdapter($dbLocal);
$dbShared = new Zend_Db_Adapter_Pdo_Mysql(array(
'host' => 'localhost',
'username' => ‘root’,
'password' => 'pass',
'dbname' => 'dbname'
));
Zend_Registry::set('db_local', $dbLocal);
Zend_Registry::set('db_shared', $dbShared);
return $dbLocal;
You can get Zend Form to translate for you just put your translation key into the label field.
$this->addElement(‘text’, ‘test’, array(‘label’ => ‘translation-key’, ‘required’ => true)); etc.
Then if you are using Zend_Db_Table_Abstract classes you can change the default schema and database connection like this:
class Default_Model_Table_Topics extends Zend_Db_Table_Abstract
{
protected $_name = 'topics';
protected $_id = 'topic_id';
protected $_rowClass = 'Default_Model_Topic';
protected $_schema = 'dbname';
protected $_adapter = 'db_shared';
}
If you need any more examples I’ll try and help.

Categories