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 
6 
7  const ACTION_EXEC = 'exec'; //!< New.
8  const ACTION_UNDO = 'undo'; //!< Removed.
9  const ACTION_REDO = 'redo'; //!< Changed.
10  const ACTION_NOTE = 'note'; //!< Notes.
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 
16  protected $_db = null;
17  protected $_currentPath = null; //!< Path with current files (keep out of version control; empty = path + "current/").
18 
19  protected $_current = null;
20 
21  protected function flatten($sql){
22  return preg_replace('/\\s+/','',$sql);
23  }
24  /**
25  * Return all migration files in a directory.
26  * @param string $path
27  * @return array Key = migration filename, value = SQL.
28  */
29  protected function dir($path){
30  $dir = [];
31  foreach((new \GlobIterator($path . $this->mask,\GlobIterator::NEW_CURRENT_AND_KEY)) as $filename => $info)
32  $dir[$filename] = file_get_contents($path . $filename);
33  ksort($dir);
34  return $dir;
35  }
36  /**
37  * Difference between current database state and desired state based on migration files.
38  * @return array Key = migration filename, value = action.
39  */
40  public function diff(){
41  $diff = array_fill_keys(array_keys($this->current),self::ACTION_UNDO);
42  $redo = false;
43  foreach($this->dir($this->path) as $filename => $sql) $diff[$filename] = array_key_exists($filename,$this->current)
44  ? ($redo || ($redo = $this->flatten($this->current[$filename]) != $this->flatten($sql)) ? self::ACTION_REDO : null) //redo all after first redo
45  : self::ACTION_EXEC;
46  ksort($diff);
47  return array_filter($diff);
48  }
49 
50  protected function split($sql){
51  $parts = [];
52  $delimiter = '/(?:$|' . str_replace('\\[action\\]','(' . implode('|',$this->constants('ACTION_')) . ')',preg_quote($this->delimiter,'/')) . ')/';
53  $action = self::ACTION_EXEC;
54  while(preg_match($delimiter,$sql,$match,PREG_OFFSET_CAPTURE)) switch(count($match)){
55  case 1: //last part
56  $parts[$action] = $sql;
57  break 2;
58  case 2:
59  $parts[$action] = substr($sql,0,$match[0][1]);
60  $sql = substr($sql,$match[1][1] + strlen($action = $match[1][0]));
61  break;
62  }
63  return $parts;
64  }
65  /**
66  * Migrate the database.
67  * @return array Key = migration filename, value = action.
68  * @see diff()
69  */
70  public function execute(&$notes = null){
71  $log = $this->component('log');
72  if($diff = $this->diff()){
73  $notes = [];
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_REDO:
85  case self::ACTION_EXEC:
86  $parts = $this->split(file_get_contents($this->path . $filename));
87  $log->info("Migrate: $action $filename",__FILE__,__LINE__,array_key_exists(self::ACTION_NOTE,$parts) ? ['notes' => $notes[$filename] = $parts[self::ACTION_NOTE]] : null);
88  if(trim($sql = $parts[$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 }
Rsi\Fred\Db\Migrate\ACTION_EXEC
const ACTION_EXEC
New.
Definition: Migrate.php:7
Rsi\Fred\Db\Migrate\ACTION_REDO
const ACTION_REDO
Changed.
Definition: Migrate.php:9
Rsi\Fred\Db\Migrate\$delimiter
$delimiter
Delimiter for the migration file parts ([action] holds the action).
Definition: Migrate.php:14
Rsi\Fred\Component
Basic component class.
Definition: Component.php:8
Rsi\Fred\Db\Migrate\$path
$path
Directory with migration files (including trailing delimiter).
Definition: Migrate.php:13
Rsi\Fred\Db\Migrate
Definition: Migrate.php:5
Rsi\Fred\Db\Migrate\$mask
$mask
Format for migration files.
Definition: Migrate.php:12
Rsi\Fred\Db\Migrate\ACTION_NOTE
const ACTION_NOTE
Notes.
Definition: Migrate.php:10
Rsi\Fred\Db
Definition: Migrate.php:3
Rsi\Fred\Db\Migrate\ACTION_UNDO
const ACTION_UNDO
Removed.
Definition: Migrate.php:8