Subversion-Projekte lars-tiefland.ci

Revision

Revision 1257 | Zur aktuellen Revision | Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
68 lars 1
<?php
2
/**
3
 * CodeIgniter
4
 *
5
 * An open source application development framework for PHP
6
 *
7
 * This content is released under the MIT License (MIT)
8
 *
9
 * Copyright (c) 2014 - 2016, British Columbia Institute of Technology
10
 *
11
 * Permission is hereby granted, free of charge, to any person obtaining a copy
12
 * of this software and associated documentation files (the "Software"), to deal
13
 * in the Software without restriction, including without limitation the rights
14
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15
 * copies of the Software, and to permit persons to whom the Software is
16
 * furnished to do so, subject to the following conditions:
17
 *
18
 * The above copyright notice and this permission notice shall be included in
19
 * all copies or substantial portions of the Software.
20
 *
21
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
27
 * THE SOFTWARE.
28
 *
29
 * @package	CodeIgniter
30
 * @author	EllisLab Dev Team
31
 * @copyright	Copyright (c) 2008 - 2014, EllisLab, Inc. (https://ellislab.com/)
32
 * @copyright	Copyright (c) 2014 - 2016, British Columbia Institute of Technology (http://bcit.ca/)
33
 * @license	http://opensource.org/licenses/MIT	MIT License
34
 * @link	https://codeigniter.com
35
 * @since	Version 3.0.0
36
 * @filesource
37
 */
38
defined('BASEPATH') OR exit('No direct script access allowed');
39
 
40
/**
41
 * Migration Class
42
 *
43
 * All migrations should implement this, forces up() and down() and gives
44
 * access to the CI super-global.
45
 *
46
 * @package		CodeIgniter
47
 * @subpackage	Libraries
48
 * @category	Libraries
49
 * @author		Reactor Engineers
50
 * @link
51
 */
52
class CI_Migration {
53
 
54
	/**
55
	 * Whether the library is enabled
56
	 *
57
	 * @var bool
58
	 */
59
	protected $_migration_enabled = FALSE;
60
 
61
	/**
62
	 * Migration numbering type
63
	 *
64
	 * @var	bool
65
	 */
66
	protected $_migration_type = 'sequential';
67
 
68
	/**
69
	 * Path to migration classes
70
	 *
71
	 * @var string
72
	 */
73
	protected $_migration_path = NULL;
74
 
75
	/**
76
	 * Current migration version
77
	 *
78
	 * @var mixed
79
	 */
80
	protected $_migration_version = 0;
81
 
82
	/**
83
	 * Database table with migration info
84
	 *
85
	 * @var string
86
	 */
87
	protected $_migration_table = 'migrations';
88
 
89
	/**
90
	 * Whether to automatically run migrations
91
	 *
92
	 * @var	bool
93
	 */
94
	protected $_migration_auto_latest = FALSE;
95
 
96
	/**
97
	 * Migration basename regex
98
	 *
99
	 * @var string
100
	 */
101
	protected $_migration_regex;
102
 
103
	/**
104
	 * Error message
105
	 *
106
	 * @var string
107
	 */
108
	protected $_error_string = '';
109
 
110
	/**
111
	 * Initialize Migration Class
112
	 *
113
	 * @param	array	$config
114
	 * @return	void
115
	 */
116
	public function __construct($config = array())
117
	{
118
		// Only run this constructor on main library load
119
		if ( ! in_array(get_class($this), array('CI_Migration', config_item('subclass_prefix').'Migration'), TRUE))
120
		{
121
			return;
122
		}
123
 
124
		foreach ($config as $key => $val)
125
		{
126
			$this->{'_'.$key} = $val;
127
		}
128
 
129
		log_message('info', 'Migrations Class Initialized');
130
 
131
		// Are they trying to use migrations while it is disabled?
132
		if ($this->_migration_enabled !== TRUE)
133
		{
134
			show_error('Migrations has been loaded but is disabled or set up incorrectly.');
135
		}
136
 
137
		// If not set, set it
138
		$this->_migration_path !== '' OR $this->_migration_path = APPPATH.'migrations/';
139
 
140
		// Add trailing slash if not set
141
		$this->_migration_path = rtrim($this->_migration_path, '/').'/';
142
 
143
		// Load migration language
144
		$this->lang->load('migration');
145
 
146
		// They'll probably be using dbforge
147
		$this->load->dbforge();
148
 
149
		// Make sure the migration table name was set.
150
		if (empty($this->_migration_table))
151
		{
152
			show_error('Migrations configuration file (migration.php) must have "migration_table" set.');
153
		}
154
 
155
		// Migration basename regex
156
		$this->_migration_regex = ($this->_migration_type === 'timestamp')
157
			? '/^\d{14}_(\w+)$/'
158
			: '/^\d{3}_(\w+)$/';
159
 
160
		// Make sure a valid migration numbering type was set.
161
		if ( ! in_array($this->_migration_type, array('sequential', 'timestamp')))
162
		{
163
			show_error('An invalid migration numbering type was specified: '.$this->_migration_type);
164
		}
165
 
166
		// If the migrations table is missing, make it
167
		if ( ! $this->db->table_exists($this->_migration_table))
168
		{
169
			$this->dbforge->add_field(array(
170
				'version' => array('type' => 'BIGINT', 'constraint' => 20),
171
			));
172
 
173
			$this->dbforge->create_table($this->_migration_table, TRUE);
174
 
175
			$this->db->insert($this->_migration_table, array('version' => 0));
176
		}
177
 
178
		// Do we auto migrate to the latest migration?
179
		if ($this->_migration_auto_latest === TRUE && ! $this->latest())
180
		{
181
			show_error($this->error_string());
182
		}
183
	}
184
 
185
	// --------------------------------------------------------------------
186
 
187
	/**
188
	 * Migrate to a schema version
189
	 *
190
	 * Calls each migration step required to get to the schema version of
191
	 * choice
192
	 *
193
	 * @param	string	$target_version	Target schema version
194
	 * @return	mixed	TRUE if no migrations are found, current version string on success, FALSE on failure
195
	 */
196
	public function version($target_version)
197
	{
198
		// Note: We use strings, so that timestamp versions work on 32-bit systems
199
		$current_version = $this->_get_version();
200
 
201
		if ($this->_migration_type === 'sequential')
202
		{
203
			$target_version = sprintf('%03d', $target_version);
204
		}
205
		else
206
		{
207
			$target_version = (string) $target_version;
208
		}
209
 
210
		$migrations = $this->find_migrations();
211
 
212
		if ($target_version > 0 && ! isset($migrations[$target_version]))
213
		{
214
			$this->_error_string = sprintf($this->lang->line('migration_not_found'), $target_version);
215
			return FALSE;
216
		}
217
 
218
		if ($target_version > $current_version)
219
		{
220
			$method = 'up';
221
		}
222
		elseif ($target_version < $current_version)
223
		{
224
			$method = 'down';
225
			// We need this so that migrations are applied in reverse order
226
			krsort($migrations);
227
		}
228
		else
229
		{
230
			// Well, there's nothing to migrate then ...
231
			return TRUE;
232
		}
233
 
234
		// Validate all available migrations within our target range.
235
		//
236
		// Unfortunately, we'll have to use another loop to run them
237
		// in order to avoid leaving the procedure in a broken state.
238
		//
239
		// See https://github.com/bcit-ci/CodeIgniter/issues/4539
240
		$pending = array();
241
		foreach ($migrations as $number => $file)
242
		{
243
			// Ignore versions out of our range.
244
			//
245
			// Because we've previously sorted the $migrations array depending on the direction,
246
			// we can safely break the loop once we reach $target_version ...
247
			if ($method === 'up')
248
			{
249
				if ($number <= $current_version)
250
				{
251
					continue;
252
				}
253
				elseif ($number > $target_version)
254
				{
255
					break;
256
				}
257
			}
258
			else
259
			{
260
				if ($number > $current_version)
261
				{
262
					continue;
263
				}
264
				elseif ($number <= $target_version)
265
				{
266
					break;
267
				}
268
			}
269
 
270
			// Check for sequence gaps
271
			if ($this->_migration_type === 'sequential')
272
			{
273
				if (isset($previous) && abs($number - $previous) > 1)
274
				{
275
					$this->_error_string = sprintf($this->lang->line('migration_sequence_gap'), $number);
276
					return FALSE;
277
				}
278
 
279
				$previous = $number;
280
			}
281
 
282
			include_once($file);
283
			$class = 'Migration_'.ucfirst(strtolower($this->_get_migration_name(basename($file, '.php'))));
284
 
285
			// Validate the migration file structure
286
			if ( ! class_exists($class, FALSE))
287
			{
288
				$this->_error_string = sprintf($this->lang->line('migration_class_doesnt_exist'), $class);
289
				return FALSE;
290
			}
291
			// method_exists() returns true for non-public methods,
292
			// while is_callable() can't be used without instantiating.
293
			// Only get_class_methods() satisfies both conditions.
294
			elseif ( ! in_array($method, array_map('strtolower', get_class_methods($class))))
295
			{
296
				$this->_error_string = sprintf($this->lang->line('migration_missing_'.$method.'_method'), $class);
297
				return FALSE;
298
			}
299
 
300
			$pending[$number] = array($class, $method);
301
		}
302
 
303
		// Now just run the necessary migrations
304
		foreach ($pending as $number => $migration)
305
		{
306
			log_message('debug', 'Migrating '.$method.' from version '.$current_version.' to version '.$number);
307
 
308
			$migration[0] = new $migration[0];
309
			call_user_func($migration);
310
			$current_version = $number;
311
			$this->_update_version($current_version);
312
		}
313
 
314
		// This is necessary when moving down, since the the last migration applied
315
		// will be the down() method for the next migration up from the target
316
		if ($current_version <> $target_version)
317
		{
318
			$current_version = $target_version;
319
			$this->_update_version($current_version);
320
		}
321
 
322
		log_message('debug', 'Finished migrating to '.$current_version);
323
		return $current_version;
324
	}
325
 
326
	// --------------------------------------------------------------------
327
 
328
	/**
329
	 * Sets the schema to the latest migration
330
	 *
331
	 * @return	mixed	Current version string on success, FALSE on failure
332
	 */
333
	public function latest()
334
	{
335
		$migrations = $this->find_migrations();
336
 
337
		if (empty($migrations))
338
		{
339
			$this->_error_string = $this->lang->line('migration_none_found');
340
			return FALSE;
341
		}
342
 
343
		$last_migration = basename(end($migrations));
344
 
345
		// Calculate the last migration step from existing migration
346
		// filenames and proceed to the standard version migration
347
		return $this->version($this->_get_migration_number($last_migration));
348
	}
349
 
350
	// --------------------------------------------------------------------
351
 
352
	/**
353
	 * Sets the schema to the migration version set in config
354
	 *
355
	 * @return	mixed	TRUE if no migrations are found, current version string on success, FALSE on failure
356
	 */
357
	public function current()
358
	{
359
		return $this->version($this->_migration_version);
360
	}
361
 
362
	// --------------------------------------------------------------------
363
 
364
	/**
365
	 * Error string
366
	 *
367
	 * @return	string	Error message returned as a string
368
	 */
369
	public function error_string()
370
	{
371
		return $this->_error_string;
372
	}
373
 
374
	// --------------------------------------------------------------------
375
 
376
	/**
377
	 * Retrieves list of available migration scripts
378
	 *
379
	 * @return	array	list of migration file paths sorted by version
380
	 */
381
	public function find_migrations()
382
	{
383
		$migrations = array();
384
 
385
		// Load all *_*.php files in the migrations path
386
		foreach (glob($this->_migration_path.'*_*.php') as $file)
387
		{
388
			$name = basename($file, '.php');
389
 
390
			// Filter out non-migration files
391
			if (preg_match($this->_migration_regex, $name))
392
			{
393
				$number = $this->_get_migration_number($name);
394
 
395
				// There cannot be duplicate migration numbers
396
				if (isset($migrations[$number]))
397
				{
398
					$this->_error_string = sprintf($this->lang->line('migration_multiple_version'), $number);
399
					show_error($this->_error_string);
400
				}
401
 
402
				$migrations[$number] = $file;
403
			}
404
		}
405
 
406
		ksort($migrations);
407
		return $migrations;
408
	}
409
 
410
	// --------------------------------------------------------------------
411
 
412
	/**
413
	 * Extracts the migration number from a filename
414
	 *
415
	 * @param	string	$migration
416
	 * @return	string	Numeric portion of a migration filename
417
	 */
418
	protected function _get_migration_number($migration)
419
	{
420
		return sscanf($migration, '%[0-9]+', $number)
421
			? $number : '0';
422
	}
423
 
424
	// --------------------------------------------------------------------
425
 
426
	/**
427
	 * Extracts the migration class name from a filename
428
	 *
429
	 * @param	string	$migration
430
	 * @return	string	text portion of a migration filename
431
	 */
432
	protected function _get_migration_name($migration)
433
	{
434
		$parts = explode('_', $migration);
435
		array_shift($parts);
436
		return implode('_', $parts);
437
	}
438
 
439
	// --------------------------------------------------------------------
440
 
441
	/**
442
	 * Retrieves current schema version
443
	 *
444
	 * @return	string	Current migration version
445
	 */
446
	protected function _get_version()
447
	{
448
		$row = $this->db->select('version')->get($this->_migration_table)->row();
449
		return $row ? $row->version : '0';
450
	}
451
 
452
	// --------------------------------------------------------------------
453
 
454
	/**
455
	 * Stores the current schema version
456
	 *
457
	 * @param	string	$migration	Migration reached
458
	 * @return	void
459
	 */
460
	protected function _update_version($migration)
461
	{
462
		$this->db->update($this->_migration_table, array(
463
			'version' => $migration
464
		));
465
	}
466
 
467
	// --------------------------------------------------------------------
468
 
469
	/**
470
	 * Enable the use of CI super-global
471
	 *
472
	 * @param	string	$var
473
	 * @return	mixed
474
	 */
475
	public function __get($var)
476
	{
477
		return get_instance()->$var;
478
	}
479
 
480
}