FRED™  3.0
FRED™: Framework for Rapid and Easy Development
Migrate.php
Go to the documentation of this file.
1 <?php
2 
3 namespace Rsi\Fred\Db;
4 
5 class Migrate extends \Rsi\Fred\Component{
6 
7  const ACTION_EXEC = 'exec'; //!< New.
8  const ACTION_UNDO = 'undo'; //!< Removed.
9  const ACTION_REDO = 'redo'; //!< Changed.
10  const ACTION_TEST = 'test'; //!< Testing.
11 
12  public $mask = '*.sql'; //!< Format for migration files.
13  public $path = null; //!< Directory with migration files (including trailing delimiter).
14  public $delimiter = '///// [action]'; //!< Delimiter for the migration file parts ([action] holds the action).
15  public $test = false; //!< Execute the 'test' part of the migration file.
16 
17  protected $_db = null;
18  protected $_currentPath = null; //!< Path with current files (keep out of version control; empty = path + "current/").
19 
20  protected $_current = null;
21 
22  protected function flatten($sql){
23  return md5(preg_replace('/\\s+/','',$sql));
24  }
25  /**
26  * Return all migration files in a directory.
27  * @param string $path
28  * @return array Key = migration filename, value = SQL.
29  */
30  protected function dir($path){
31  $dir = [];
32  foreach((new \GlobIterator($path . $this->mask,\GlobIterator::NEW_CURRENT_AND_KEY)) as $filename => $info)
33  $dir[$filename] = file_get_contents($path . $filename);
34  ksort($dir);
35  return $dir;
36  }
37  /**
38  * Difference between current database state and desired state based on migration files.
39  * @return array Key = migration filename, value = action.
40  */
41  public function diff(){
42  $diff = array_fill_keys(array_keys($this->current),self::ACTION_UNDO);
43  $redo = false;
44  foreach($this->dir($this->path) as $filename => $sql) $diff[$filename] = array_key_exists($filename,$this->current)
45  ? ($redo || ($redo = $this->flatten($this->current[$filename]) != $this->flatten($sql)) ? self::ACTION_REDO : null) //redo all after first redo
46  : self::ACTION_EXEC;
47  ksort($diff);
48  return array_filter($diff);
49  }
50 
51  protected function split($sql){
52  $parts = [];
53  $delimiter = '/(?:$|' . str_replace('\\[action\\]','(' . implode('|',$this->constants('ACTION_')) . ')',preg_quote($this->delimiter,'/')) . ')/';
54  $action = self::ACTION_EXEC;
55  while(preg_match($delimiter,$sql,$match,PREG_OFFSET_CAPTURE)) switch(count($match)){
56  case 1: //last part
57  $parts[$action] = $sql;
58  break 2;
59  case 2:
60  $parts[$action] = substr($sql,0,$match[0][1]);
61  $sql = substr($sql,$match[1][1] + strlen($action = $match[1][0]));
62  break;
63  }
64  return $parts;
65  }
66  /**
67  * Migrate the database.
68  * @return array Key = migration filename, value = action.
69  * @see diff()
70  */
71  public function execute(){
72  $log = $this->component('log');
73  if($diff = $this->diff()){
74  $this->_db->begin();
75  try{
76  foreach(array_reverse($diff) as $filename => $action) switch($action){
77  case self::ACTION_UNDO:
78  case self::ACTION_REDO:
79  $log->info("Migrate: undo $filename",__FILE__,__LINE__);
80  if(trim($sql = $this->split($this->current[$filename])[$action] ?? null)) $this->_db->execute($sql);
81  break;
82  }
83  foreach($diff as $filename => $action) switch($action){
84  case self::ACTION_TEST: if(!$this->test) break;
85  case self::ACTION_REDO:
86  case self::ACTION_EXEC:
87  $log->info("Migrate: $action $filename",__FILE__,__LINE__);
88  if(trim($sql = $this->split(file_get_contents($this->path . $filename))[$action] ?? null)) $this->_db->execute($sql);
89  break;
90  }
91  $this->current = $this->dir($this->path);
92  $this->_db->commit();
93  }
94  catch(\Exception $e){
95  $this->_db->rollBack();
96  throw $e;
97  }
98  }
99  return $diff;
100  }
101 
102  protected function getCurrent(){
103  if($this->_current === null) $this->_current = $this->dir($this->currentPath);
104  return $this->_current;
105  }
106 
107  protected function setCurrent($value){
108  foreach($this->dir($this->current) as $filename => $sql) unlink($this->currentPath . $filename);
109  foreach(($this->_current = $value) as $filename => $sql) file_put_contents($this->currentPath . $filename,$sql);
110  }
111 
112  protected function getCurrentPath(){
113  return $this->_currentPath ?: $this->path . 'current/';
114  }
115 
116 }
$delimiter
Delimiter for the migration file parts ([action] holds the action).
Definition: Migrate.php:14
$path
Directory with migration files (including trailing delimiter).
Definition: Migrate.php:13
const ACTION_REDO
Changed.
Definition: Migrate.php:9
const ACTION_UNDO
Removed.
Definition: Migrate.php:8
const ACTION_TEST
Testing.
Definition: Migrate.php:10
$mask
Format for migration files.
Definition: Migrate.php:12
const ACTION_EXEC
New.
Definition: Migrate.php:7