Development

/branches/1.0/lib/addon/propel/sfPropelData.class.php

You must first sign up to be able to contribute.

root/branches/1.0/lib/addon/propel/sfPropelData.class.php

Revision 12958, 13.4 kB (checked in by hartym, 5 years ago)

[1.0] r12811 introduced a regression: fixtures order is totally random, as sfFinder returns directories entry in arbitrary order.

  • Property svn:mime-type set to text/x-php
  • Property svn:eol-style set to native
  • Property svn:keywords set to Id Rev Date
Line 
1 <?php
2
3 /*
4  * This file is part of the symfony package.
5  * (c) 2004-2006 Fabien Potencier <fabien.potencier@symfony-project.com>
6  *
7  * For the full copyright and license information, please view the LICENSE
8  * file that was distributed with this source code.
9  */
10
11 /**
12  * This class is the Propel implementation of sfData.  It interacts with the data source
13  * and loads data.
14  *
15  * @package    symfony
16  * @subpackage addon
17  * @author     Fabien Potencier <fabien.potencier@symfony-project.com>
18  * @version    SVN: $Id$
19  */
20 class sfPropelData extends sfData
21 {
22   protected
23     $maps           = array(),
24     $deletedClasses = array(),
25     $con            = null;
26
27   // symfony load-data (file|dir)
28   /**
29    * Loads data from a file or directory into a Propel data source
30    *
31    * @param mixed A file or directory path
32    * @param string The Propel connection name, default 'propel'
33    *
34    * @throws Exception If the database throws an error, rollback transaction and rethrows exception
35    */
36   public function loadData($directory_or_file = null, $connectionName = 'propel')
37   {
38     $fixture_files = $this->getFiles($directory_or_file);
39
40     // wrap all database operations in a single transaction
41     $this->con = Propel::getConnection($connectionName);
42     try
43     {
44       $this->con->begin();
45
46       $this->doDeleteCurrentData($fixture_files);
47
48       $this->doLoadData($fixture_files);
49
50       $this->con->commit();
51     }
52     catch (Exception $e)
53     {
54       $this->con->rollback();
55       throw $e;
56     }
57   }
58
59   /**
60    * Implements the abstract loadDataFromArray method and loads the data using the generated data model.
61    *
62    * @param array The data to be loaded into the data source
63    *
64    * @throws Exception If data is unnamed.
65    * @throws sfException If an object defined in the model does not exist in the data
66    * @throws sfException If a column that does not exist is referenced
67    */
68   public function loadDataFromArray($data)
69   {
70     if ($data === null)
71     {
72       // no data
73       return;
74     }
75
76     foreach ($data as $class => $datas)
77     {
78       $class = trim($class);
79
80       $peer_class = $class.'Peer';
81
82       // load map class
83       $this->loadMapBuilder($class);
84
85       $tableMap = $this->maps[$class]->getDatabaseMap()->getTable(constant($peer_class.'::TABLE_NAME'));
86
87       $column_names = call_user_func_array(array($peer_class, 'getFieldNames'), array(BasePeer::TYPE_FIELDNAME));
88
89       // iterate through datas for this class
90       // might have been empty just for force a table to be emptied on import
91       if (!is_array($datas))
92       {
93         continue;
94       }
95
96       foreach ($datas as $key => $data)
97       {
98         // create a new entry in the database
99         $obj = new $class();
100
101         if (!$obj instanceof BaseObject)
102         {
103           throw new Exception(sprintf('The class "%s" is not a Propel class. This probably means there is already a class named "%s" somewhere in symfony or in your project.', $class, $class));
104         }
105
106         if (!is_array($data))
107         {
108           throw new Exception(sprintf('You must give a name for each fixture data entry (class %s)', $class));
109         }
110
111         foreach ($data as $name => $value)
112         {
113           $isARealColumn = true;
114           try
115           {
116             $column = $tableMap->getColumn($name);
117           }
118           catch (PropelException $e)
119           {
120             $isARealColumn = false;
121           }
122
123           // foreign key?
124           if ($isARealColumn)
125           {
126             if ($column->isForeignKey() && !is_null($value))
127             {
128               $relatedTable = $this->maps[$class]->getDatabaseMap()->getTable($column->getRelatedTableName());
129
130               if (!isset($this->object_references[$relatedTable->getPhpName().'_'.$value]))
131               {
132                 throw new sfException(sprintf('The object "%s" from class "%s" is not defined in your data file.', $value, $relatedTable->getPhpName()));
133               }
134
135               $value = $this->object_references[$relatedTable->getPhpName().'_'.$value];
136             }
137           }
138
139           if (false !== $pos = array_search($name, $column_names))
140           {
141             $obj->setByPosition($pos, $value);
142           }
143           else if (is_callable(array($obj, $method = 'set'.sfInflector::camelize($name))))
144           {
145             $obj->$method($value);
146           }
147           else
148           {
149             $error = 'Column "%s" does not exist for class "%s"';
150             $error = sprintf($error, $name, $class);
151             throw new sfException($error);
152           }
153         }
154         $obj->save($this->con);
155
156         // save the id for future reference
157         if (method_exists($obj, 'getPrimaryKey'))
158         {
159           $this->object_references[$class.'_'.$key] = $obj->getPrimaryKey();
160         }
161       }
162     }
163   }
164
165   /**
166    * Clears existing data from the data source by reading the fixture files
167    * and deleting the existing data for only those classes that are mentioned
168    * in the fixtures.
169    *
170    * @param array The list of YAML files.
171    *
172    * @throws sfException If a class mentioned in a fixture can not be found
173    */
174   protected function doDeleteCurrentData($fixture_files)
175   {
176     // delete all current datas in database
177     if (!$this->deleteCurrentData)
178     {
179       return;
180     }
181
182     rsort($fixture_files);
183     foreach ($fixture_files as $fixture_file)
184     {
185       $data = sfYaml::load($fixture_file);
186
187       if ($data === null)
188       {
189         // no data
190         continue;
191       }
192
193       $classes = array_keys($data);
194       foreach (array_reverse($classes) as $class)
195       {
196         $class = trim($class);
197         if (in_array($class, $this->deletedClasses))
198         {
199           continue;
200         }
201
202         $peer_class = $class.'Peer';
203
204         if (!$classPath = sfCore::getClassPath($peer_class))
205         {
206           throw new sfException(sprintf('Unable to find path for class "%s".', $peer_class));
207         }
208
209         require_once($classPath);
210
211         call_user_func(array($peer_class, 'doDeleteAll'), $this->con);
212
213         $this->deletedClasses[] = $class;
214       }
215     }
216   }
217
218   /**
219    * Loads the mappings for the classes
220    *
221    * @param string The model class name
222    *
223    * @throws sfException If the class cannot be found
224    */
225   protected function loadMapBuilder($class)
226   {
227     $mapBuilderClass = $class.'MapBuilder';
228     if (!isset($this->maps[$class]))
229     {
230       if (!$classPath = sfCore::getClassPath($mapBuilderClass))
231       {
232         throw new sfException(sprintf('Unable to find path for class "%s".', $mapBuilderClass));
233       }
234
235       require_once($classPath);
236       $this->maps[$class] = new $mapBuilderClass();
237       $this->maps[$class]->doBuild();
238     }
239   }
240
241   /**
242    * Dumps data to fixture from one or more tables.
243    *
244    * @param string directory or file to dump to
245    * @param mixed  name or names of tables to dump (or all to dump all tables)
246    * @param string connection name
247    */
248   public function dumpData($directory_or_file = null, $tables = 'all', $connectionName = 'propel')
249   {
250     $sameFile = true;
251     if (is_dir($directory_or_file))
252     {
253       // multi files
254       $sameFile = false;
255     }
256     else
257     {
258       // same file
259       // delete file
260     }
261
262     $this->con = Propel::getConnection($connectionName);
263
264     // get tables
265     if ('all' === $tables || is_null($tables))
266     {
267       // load all map builder classes
268       $files = sfFinder::type('file')->ignore_version_control()->name('*MapBuilder.php')->in(sfLoader::getModelDirs());
269       foreach ($files as $file)
270       {
271         $mapBuilderClass = basename($file, '.php');
272         $map = new $mapBuilderClass();
273         $map->doBuild();
274       }
275
276       $dbMap = Propel::getDatabaseMap($connectionName);
277       $tables = array();
278       foreach ($dbMap->getTables() as $table)
279       {
280         $tables[] = $table->getPhpName();
281       }
282     }
283     else if (!is_array($tables))
284     {
285       $tables = array($tables);
286     }
287
288     $dumpData = array();
289
290     // load map classes
291     array_walk($tables, array($this, 'loadMapBuilder'));
292
293     $tables = $this->fixOrderingOfForeignKeyData($tables);
294     foreach ($tables as $tableName)
295     {
296       $tableMap = $this->maps[$tableName]->getDatabaseMap()->getTable(constant($tableName.'Peer::TABLE_NAME'));
297       $hasParent = false;
298       $haveParents = false;
299       $fixColumn = null;
300       foreach ($tableMap->getColumns() as $column)
301       {
302         $col = strtolower($column->getColumnName());
303         if ($column->isForeignKey())
304         {
305           $relatedTable = $this->maps[$tableName]->getDatabaseMap()->getTable($column->getRelatedTableName());
306           if ($tableName === $relatedTable->getPhpName())
307           {
308             if ($hasParent)
309             {
310               $haveParents = true;
311             }
312             else
313             {
314               $fixColumn = $column;
315               $hasParent = true;
316             }
317           }
318         }
319       }
320
321       if ($haveParents)
322       {
323         // unable to dump tables having multi-recursive references
324         continue;
325       }
326
327       // get db info
328       $resultsSets = array();
329       if ($hasParent)
330       {
331         $resultsSets = $this->fixOrderingOfForeignKeyDataInSameTable($resultsSets, $tableName, $fixColumn);
332       }
333       else
334       {
335         $resultsSets[] = $this->con->executeQuery('SELECT * FROM '.constant($tableName.'Peer::TABLE_NAME'));
336       }
337
338       foreach ($resultsSets as $rs)
339       {
340         if($rs->getRecordCount() > 0 && !isset($dumpData[$tableName])){
341           $dumpData[$tableName] = array();
342         }
343
344         while ($rs->next())
345         {
346           $pk = $tableName;
347           $values = array();
348           $primaryKeys = array();
349           $foreignKeys = array();
350
351           foreach ($tableMap->getColumns() as $column)
352           {
353             $col = strtolower($column->getColumnName());
354             $isPrimaryKey = $column->isPrimaryKey();
355
356             if (is_null($rs->get($col)))
357             {
358               continue;
359             }
360
361             if ($isPrimaryKey)
362             {
363               $value = $rs->get($col);
364               $pk .= '_'.$value;
365               $primaryKeys[$col] = $value;
366             }
367
368             if ($column->isForeignKey())
369             {
370               $relatedTable = $this->maps[$tableName]->getDatabaseMap()->getTable($column->getRelatedTableName());
371               if ($isPrimaryKey)
372               {
373                 $foreignKeys[$col] = $rs->get($col);
374                 $primaryKeys[$col] = $relatedTable->getPhpName().'_'.$rs->get($col);
375               }
376               else
377               {
378                 $values[$col] = $relatedTable->getPhpName().'_'.$rs->get($col);
379               }
380             }
381             elseif (!$isPrimaryKey || ($isPrimaryKey && !$tableMap->isUseIdGenerator()))
382             {
383               // We did not want auto incremented primary keys
384               $values[$col] = $rs->get($col);
385             }
386           }
387
388           if (count($primaryKeys) > 1 || (count($primaryKeys) > 0 && count($foreignKeys) > 0))
389           {
390             $values = array_merge($primaryKeys, $values);
391           }
392
393           $dumpData[$tableName][$pk] = $values;
394         }
395       }
396     }
397
398     // save to file(s)
399     if ($sameFile)
400     {
401       file_put_contents($directory_or_file, Spyc::YAMLDump($dumpData));
402     }
403     else
404     {
405       $i = 0;
406       foreach ($tables as $tableName)
407       {
408         if (!isset($dumpData[$tableName]))
409         {
410           continue;
411         }
412
413         file_put_contents(sprintf("%s/%03d-%s.yml", $directory_or_file, ++$i, $tableName), Spyc::YAMLDump(array($tableName => $dumpData[$tableName])));
414       }
415     }
416   }
417
418   /**
419    * Fixes the ordering of foreign key data, by outputting data a foreign key depends on before the table with the foreign key.
420    *
421    * @param array The array with the class names.
422    */
423   public function fixOrderingOfForeignKeyData($classes)
424   {
425     // reordering classes to take foreign keys into account
426     for ($i = 0, $count = count($classes); $i < $count; $i++)
427     {
428       $class = $classes[$i];
429       $tableMap = $this->maps[$class]->getDatabaseMap()->getTable(constant($class.'Peer::TABLE_NAME'));
430       foreach ($tableMap->getColumns() as $column)
431       {
432         if ($column->isForeignKey())
433         {
434           $relatedTable = $this->maps[$class]->getDatabaseMap()->getTable($column->getRelatedTableName());
435           $relatedTablePos = array_search($relatedTable->getPhpName(), $classes);
436
437           // check if relatedTable is after the current table
438           if ($relatedTablePos > $i)
439           {
440             // move related table 1 position before current table
441             $classes = array_merge(
442               array_slice($classes, 0, $i),
443               array($classes[$relatedTablePos]),
444               array_slice($classes, $i, $relatedTablePos - $i),
445               array_slice($classes, $relatedTablePos + 1)
446             );
447
448             // we have moved a table, so let's see if we are done
449             return $this->fixOrderingOfForeignKeyData($classes);
450           }
451         }
452       }
453     }
454
455     return $classes;
456   }
457
458   protected function fixOrderingOfForeignKeyDataInSameTable($resultsSets, $tableName, $column, $in = null)
459   {
460     $rs = $this->con->executeQuery(sprintf('SELECT * FROM %s WHERE %s %s',
461       constant($tableName.'Peer::TABLE_NAME'),
462       strtolower($column->getColumnName()),
463       is_null($in) ? 'IS NULL' : 'IN ('.$in.')'
464     ));
465     $in = array();
466     while ($rs->next())
467     {
468       $in[] = "'".$rs->get(strtolower($column->getRelatedColumnName()))."'";
469     }
470
471     if ($in = implode(', ', $in))
472     {
473       $rs->seek(0);
474       $resultsSets[] = $rs;
475       $resultsSets = $this->fixOrderingOfForeignKeyDataInSameTable($resultsSets, $tableName, $column, $in);
476     }
477
478     return $resultsSets;
479   }
480 }
481
Note: See TracBrowser for help on using the browser.