Implemented the fixtures dumper command

This commit is contained in:
Kévin Gomez 2013-11-05 11:23:36 +00:00
parent 224d00b50d
commit f16c92580f
8 changed files with 602 additions and 1 deletions

View file

@ -0,0 +1,112 @@
<?php
/**
* This file is part of the PropelBundle package.
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @license MIT License
*/
namespace Propel\PropelBundle\Command;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Filesystem\Filesystem;
/**
* FixturesDumpCommand.
*
* @author William DURAND <william.durand1@gmail.com>
*/
class FixturesDumpCommand extends AbstractCommand
{
/**
* Default fixtures directory.
* @var string
*/
protected $defaultFixturesDir = 'app/propel/fixtures';
/**
* @see Command
*/
protected function configure()
{
$this
->setName('propel:fixtures:dump')
->setDescription('Dump data from database into YAML fixtures file.')
->setHelp(<<<EOT
The <info>propel:fixtures:dump</info> dumps data from database into YAML fixtures file.
<info>php app/console propel:fixtures:dump</info>
The <info>--connection</info> parameter allows you to change the connection to use.
The <info>--dir</info> parameter allows you to change the output directory.
The default connection is the active connection (propel.dbal.default_connection).
EOT
)
->addOption('connection', null, InputOption::VALUE_OPTIONAL, 'Set this parameter to define a connection to use')
->addOption('dir', null, InputOption::VALUE_OPTIONAL, 'Set this parameter to define a fixture directory')
;
}
/**
* @see Command
*
* @throws \InvalidArgumentException When the target directory does not exist
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$fixtureDir = $input->getOption('dir') ?: $this->defaultFixturesDir;
$path = realpath($this->getApplication()->getKernel()->getRootDir() . '/../') . '/' . $fixtureDir;
if (!file_exists($path)) {
$output->writeln("<info>The $path folder does not exists.</info>");
if ($this->askConfirmation($output, "<question>Do you want me to create it for you ?</question> [Yes]")) {
$fs = new Filesystem();
$fs->mkdir($path);
$this->writeNewDirectory($output, $path);
} else {
throw new \IOException(sprintf('Unable to find the %s folder', $path));
}
}
$filename = $path . '/fixtures_' . time() . '.yml';
$dumper = $this->getContainer()->get('propel.dumper.yaml');
try {
$dumper->dump($filename, $input->getOption('connection'));
} catch (\Exception $e) {
$this->writeSection($output, array(
'[Propel] Exception',
'',
$e->getMessage()), 'fg=white;bg=red');
return false;
}
$this->writeNewFile($output, $filename);
return true;
}
/**
* {@inheritdoc}
*/
protected function createSubCommandInstance()
{
// useless here
}
/**
* {@inheritdoc}
*/
protected function getSubCommandArguments(InputInterface $input)
{
// useless here
}
}

View file

@ -45,4 +45,22 @@ trait FormattingHelpers
{
return $this->getHelperSet()->get('dialog')->askConfirmation($output, $question, $default);
}
/**
* @param OutputInterface $output The output.
* @param string $filename The filename.
*/
protected function writeNewFile(OutputInterface $output, $filename)
{
$output->writeln('>> <info>File+</info> ' . $filename);
}
/**
* @param OutputInterface $output The output.
* @param string $directory The directory.
*/
protected function writeNewDirectory(OutputInterface $output, $directory)
{
$output->writeln('>> <info>Dir+</info> ' . $directory);
}
}

View file

@ -0,0 +1,157 @@
<?php
/**
* This file is part of the PropelBundle package.
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @license MIT License
*/
namespace Propel\PropelBundle\DataFixtures;
use Propel\Runtime\Propel;
use Symfony\Component\Finder\Finder;
/**
* @author William Durand <william.durand1@gmail.com>
*/
abstract class AbstractDataHandler
{
/**
* @var string
*/
protected $rootDir;
/**
* @var \PDO
*/
protected $con;
/**
* @var \DatabaseMap
*/
protected $dbMap;
/**
* @var Propel
*/
protected $propel;
/**
* @var array
*/
protected $datasources = array();
/**
* Default constructor
*
* @param string $rootDir The root directory.
*/
public function __construct($rootDir, Propel $propel, array $datasources)
{
$this->rootDir = $rootDir;
$this->propel = $propel;
$this->datasources = $datasources;
}
/**
* @return string
*/
protected function getRootDir()
{
return $this->rootDir;
}
/**
* Load Map builders.
*
* @param string $connectionName A connection name.
*/
protected function loadMapBuilders($connectionName = null)
{
if (null !== $this->dbMap) {
return;
}
$this->dbMap = $this->propel->getDatabaseMap($connectionName);
if (0 === count($this->dbMap->getTables())) {
$finder = new Finder();
$files = $finder
->files()->name('*TableMap.php')
->in($this->getModelSearchPaths($connectionName))
->notName('TableMap.php')
->exclude('PropelBundle')
->exclude('Tests');
foreach ($files as $file) {
$class = $this->guessFullClassName($file->getRelativePath(), basename($file, '.php'));
if (null !== $class && $this->isInDatabase($class, $connectionName)) {
$this->dbMap->addTableFromMapClass($class);
}
}
}
}
/**
* Check if a table is in a database
* @param string $class
* @param string $connectionName
* @return boolean
*/
protected function isInDatabase($class, $connectionName)
{
return constant($class.'::DATABASE_NAME') === $connectionName;
}
/**
* Try to find a valid class with its namespace based on the filename.
* Based on the PSR-0 standard, the namespace should be the directory structure.
*
* @param string $path The relative path of the file.
* @param string $shortClassName The short class name aka the filename without extension.
*/
private function guessFullClassName($path, $shortClassName)
{
$array = array();
$path = str_replace('/', '\\', $path);
$array[] = $path;
while ($pos = strpos($path, '\\')) {
$path = substr($path, $pos + 1, strlen($path));
$array[] = $path;
}
$array = array_reverse($array);
while ($ns = array_pop($array)) {
$class = $ns . '\\' . $shortClassName;
if (class_exists($class)) {
return $class;
}
}
return null;
}
/**
* Gets the search path for models out of the configuration.
*
* @param string $connectionName A connection name.
*
* @return string[]
*/
protected function getModelSearchPaths($connectionName) {
$searchPath = array();
if (!empty($this->datasources[$connectionName]['connection']['model_paths'])) {
$modelPaths = $this->datasources[$connectionName]['connection']['model_paths'];
foreach ($modelPaths as $modelPath) {
$searchPath[] = $this->getRootDir() . '/../' . $modelPath;
}
} else {
$searchPath[] = $this->getRootDir() . '/../';
}
return $searchPath;
}
}

View file

@ -0,0 +1,241 @@
<?php
/**
* This file is part of the PropelBundle package.
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @license MIT License
*/
namespace Propel\PropelBundle\DataFixtures\Dumper;
use \Pdo;
use Propel\PropelBundle\DataFixtures\AbstractDataHandler;
use Propel\Runtime\Util\PropelColumnTypes;
/**
* Abstract class to manage a common logic to dump data.
*
* @author William Durand <william.durand1@gmail.com>
*/
abstract class AbstractDataDumper extends AbstractDataHandler implements DataDumperInterface
{
/**
* {@inheritdoc}
*/
public function dump($filename, $connectionName = null)
{
if (null === $filename || '' === $filename) {
throw new \Exception('Invalid filename provided.');
}
$this->loadMapBuilders($connectionName);
$this->con = $this->propel->getConnection($connectionName);
$array = $this->getDataAsArray($connectionName);
$data = $this->transformArrayToData($array);
if (false === file_put_contents($filename, $data)) {
throw new \Exception(sprintf('Cannot write file: %s', $filename));
}
}
/**
* Transforms an array of data to a specific format
* depending on the specialized dumper. It should return
* a string content ready to write in a file.
*
* @return string
*/
abstract protected function transformArrayToData($data);
/**
* Dumps data to fixture from a given connection and
* returns an array.
*
* @param string $connectionName The connection name
* @return array
*/
protected function getDataAsArray()
{
$tables = array();
foreach ($this->dbMap->getTables() as $table) {
$tables[] = $table->getClassname();
}
$tables = $this->fixOrderingOfForeignKeyData($tables);
$dumpData = array();
foreach ($tables as $tableName) {
$tableMap = $this->dbMap->getTable(constant(constant($tableName.'::TABLE_MAP').'::TABLE_NAME'));
$hasParent = false;
$haveParents = false;
$fixColumn = null;
$shortTableName = substr($tableName, strrpos($tableName, '\\') + 1, strlen($tableName));
foreach ($tableMap->getColumns() as $column) {
$col = strtolower($column->getName());
if ($column->isForeignKey()) {
$relatedTable = $this->dbMap->getTable($column->getRelatedTableName());
if ($tableName === $relatedTable->getPhpName()) {
if ($hasParent) {
$haveParents = true;
} else {
$fixColumn = $column;
$hasParent = true;
}
}
}
}
if ($haveParents) {
// unable to dump tables having multi-recursive references
continue;
}
// get db info
$resultsSets = array();
if ($hasParent) {
$resultsSets[] = $this->fixOrderingOfForeignKeyDataInSameTable($resultsSets, $tableName, $fixColumn);
} else {
$in = array();
foreach ($tableMap->getColumns() as $column) {
$in[] = strtolower($column->getName());
}
$stmt = $this
->con
->query(sprintf('SELECT %s FROM %s', implode(',', $in), constant(constant($tableName.'::TABLE_MAP').'::TABLE_NAME')));
$set = array();
while (($row = $stmt->fetch(PDO::FETCH_ASSOC))) {
$set[] = $row;
}
$resultsSets[] = $set;
$stmt->close();
unset($stmt);
}
foreach ($resultsSets as $rows) {
if (count($rows) > 0 && !isset($dumpData[$tableName])) {
$dumpData[$tableName] = array();
foreach ($rows as $row) {
$pk = $shortTableName;
$values = array();
$primaryKeys = array();
$foreignKeys = array();
foreach ($tableMap->getColumns() as $column) {
$col = strtolower($column->getName());
$isPrimaryKey = $column->isPrimaryKey();
if (null === $row[$col]) {
continue;
}
if ($isPrimaryKey) {
$value = $row[$col];
$pk .= '_'.$value;
$primaryKeys[$col] = $value;
}
if ($column->isForeignKey()) {
$relatedTable = $this->dbMap->getTable($column->getRelatedTableName());
if ($isPrimaryKey) {
$foreignKeys[$col] = $row[$col];
$primaryKeys[$col] = $relatedTable->getPhpName().'_'.$row[$col];
} else {
$values[$col] = $relatedTable->getPhpName().'_'.$row[$col];
$values[$col] = strlen($row[$col]) ? $relatedTable->getPhpName().'_'.$row[$col] : '';
}
} elseif (!$isPrimaryKey || ($isPrimaryKey && !$tableMap->isUseIdGenerator())) {
if (!empty($row[$col]) && PropelColumnTypes::PHP_ARRAY === $column->getType()) {
$serialized = substr($row[$col], 2, -2);
$row[$col] = $serialized ? explode(' | ', $serialized) : array();
}
// We did not want auto incremented primary keys
$values[$col] = $row[$col];
}
if (PropelColumnTypes::OBJECT === $column->getType()) {
$values[$col] = unserialize($row[$col]);
}
}
if (count($primaryKeys) > 1 || (count($primaryKeys) > 0 && count($foreignKeys) > 0)) {
$values = array_merge($primaryKeys, $values);
}
$dumpData[$tableName][$pk] = $values;
}
}
}
}
return $dumpData;
}
/**
* Fixes the ordering of foreign key data, by outputting data
* a foreign key depends on before the table with the foreign key.
*
* @param array $classes The array with the class names
* @return array
*/
protected function fixOrderingOfForeignKeyData($classes)
{
// reordering classes to take foreign keys into account
for ($i = 0, $count = count($classes); $i < $count; $i++) {
$class = $classes[$i];
$tableMap = $this->dbMap->getTable(constant(constant($class.'::TABLE_MAP').'::TABLE_NAME'));
foreach ($tableMap->getColumns() as $column) {
if ($column->isForeignKey()) {
$relatedTable = $this->dbMap->getTable($column->getRelatedTableName());
$relatedTablePos = array_search($relatedTable->getClassname(), $classes);
// check if relatedTable is after the current table
if ($relatedTablePos > $i) {
// move related table 1 position before current table
$classes = array_merge(
array_slice($classes, 0, $i),
array($classes[$relatedTablePos]),
array_slice($classes, $i, $relatedTablePos - $i),
array_slice($classes, $relatedTablePos + 1)
);
// we have moved a table, so let's see if we are done
return $this->fixOrderingOfForeignKeyData($classes);
}
}
}
}
return $classes;
}
protected function fixOrderingOfForeignKeyDataInSameTable($resultsSets, $tableName, $column, $in = null)
{
$sql = sprintf('SELECT * FROM %s WHERE %s %s',
constant(constant($tableName.'::TABLE_MAP').'::TABLE_NAME'),
strtolower($column->getName()),
null === $in ? 'IS NULL' : 'IN ('.$in.')');
$stmt = $this->con->prepare($sql);
$stmt->execute();
$in = array();
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
$in[] = "'".$row[strtolower($column->getRelatedColumnName())]."'";
$resultsSets[] = $row;
}
if ($in = implode(', ', $in)) {
$resultsSets = $this->fixOrderingOfForeignKeyDataInSameTable($resultsSets, $tableName, $column, $in);
}
return $resultsSets;
}
}

View file

@ -0,0 +1,27 @@
<?php
/**
* This file is part of the PropelBundle package.
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @license MIT License
*/
namespace Propel\PropelBundle\DataFixtures\Dumper;
/**
* Interface that exposes how Propel data dumpers should work.
*
* @author William Durand <william.durand1@gmail.com>
*/
interface DataDumperInterface
{
/**
* Dumps data to fixtures from a given connection.
*
* @param string $filename The file name to write data.
* @param string $connectionName The Propel connection name.
*/
public function dump($filename, $connectionName = null);
}

View file

@ -0,0 +1,35 @@
<?php
/**
* This file is part of the PropelBundle package.
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @license MIT License
*/
namespace Propel\PropelBundle\DataFixtures\Dumper;
use Symfony\Component\Yaml\Yaml;
/**
* YAML fixtures dumper.
*
* @author William Durand <william.durand1@gmail.com>
*/
class YamlDataDumper extends AbstractDataDumper
{
/**
* {@inheritdoc}
*/
protected function transformArrayToData($data)
{
return Yaml::dump(
$data,
$inline = 3,
$indent = 4,
$exceptionOnInvalidType = false,
$objectSupport = true
);
}
}

View file

@ -99,7 +99,7 @@ class PropelExtension extends Extension
$c[$name]['slaves']['connection'] = $conf['slaves'];
}
foreach (array('dsn', 'user', 'password', 'classname', 'options', 'attributes', 'settings') as $att) {
foreach (array('dsn', 'user', 'password', 'classname', 'options', 'attributes', 'settings', 'model_paths') as $att) {
if (isset($conf[$att])) {
$c[$name]['connection'][$att] = $conf[$att];
}

View file

@ -7,14 +7,19 @@
<parameters>
<parameter key="propel.dbal.default_connection">default</parameter>
<parameter key="propel.class">Propel\Runtime\Propel</parameter>
<parameter key="propel.schema_locator.class">Propel\PropelBundle\Service\SchemaLocator</parameter>
<parameter key="propel.data_collector.class">Propel\PropelBundle\DataCollector\PropelDataCollector</parameter>
<parameter key="propel.logger.class">Propel\PropelBundle\Logger\PropelLogger</parameter>
<parameter key="propel.twig.extension.syntax.class">Propel\PropelBundle\Twig\Extension\SyntaxExtension</parameter>
<parameter key="form.type_guesser.propel.class">Propel\PropelBundle\Form\TypeGuesser</parameter>
<parameter key="propel.dumper.yaml.class">Propel\PropelBundle\DataFixtures\Dumper\YamlDataDumper</parameter>
</parameters>
<services>
<service id="propel" class="%propel.class%">
</service>
<service id="propel.schema_locator" class="%propel.schema_locator.class%">
<argument type="service" id="file_locator" />
</service>
@ -36,5 +41,11 @@
<service id="form.type_guesser.propel" class="%form.type_guesser.propel.class%">
<tag name="form.type_guesser" />
</service>
<service id="propel.dumper.yaml" class="%propel.dumper.yaml.class%">
<argument>%kernel.root_dir%</argument>
<argument type="service" id="propel" />
<argument>%propel.configuration%</argument>
</service>
</services>
</container>