Cake/Console/Command/SchemaShell.php

1 <?php
2 /**
3 * Command-line database management utility to automate programmer chores.
4 *
5 * Schema is CakePHP's database management utility. This helps you maintain versions of
6 * of your database.
7 *
8 * PHP 5
9 *
10 * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
11 * Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
12 *
13 * Licensed under The MIT License
14 * Redistributions of files must retain the above copyright notice.
15 *
16 * @copyright Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
17 * @link http://cakephp.org CakePHP(tm) Project
18 * @since CakePHP(tm) v 1.2.0.5550
19 * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
20 */
21  
22 App::uses('AppShell', 'Console/Command');
23 App::uses('File', 'Utility');
24 App::uses('Folder', 'Utility');
25 App::uses('CakeSchema', 'Model');
26  
27 /**
28 * Schema is a command-line database management utility for automating programmer chores.
29 *
30 * Schema is CakePHP's database management utility. This helps you maintain versions of
31 * of your database.
32 *
33 * @package Cake.Console.Command
34 * @link http://book.cakephp.org/2.0/en/console-and-shells/schema-management-and-migrations.html
35 */
36 class SchemaShell extends AppShell {
37  
38 /**
39 * Schema class being used.
40 *
41 * @var CakeSchema
42 */
43 public $Schema;
44  
45 /**
46 * is this a dry run?
47 *
48 * @var boolean
49 */
50 protected $_dry = null;
51  
52 /**
53 * Override startup
54 *
55 * @return void
56 */
57 public function startup() {
58 $this->_welcome();
59 $this->out('Cake Schema Shell');
60 $this->hr();
61  
62 $name = $path = $connection = $plugin = null;
63 if (!empty($this->params['name'])) {
64 $name = $this->params['name'];
65 } elseif (!empty($this->args[0]) && $this->args[0] !== 'snapshot') {
66 $name = $this->params['name'] = $this->args[0];
67 }
68  
69 if (strpos($name, '.')) {
70 list($this->params['plugin'], $splitName) = pluginSplit($name);
71 $name = $this->params['name'] = $splitName;
72 }
73  
74 if ($name) {
75 $this->params['file'] = Inflector::underscore($name);
76 }
77  
78 if (empty($this->params['file'])) {
79 $this->params['file'] = 'schema.php';
80 }
81 if (strpos($this->params['file'], '.php') === false) {
82 $this->params['file'] .= '.php';
83 }
84 $file = $this->params['file'];
85  
86 if (!empty($this->params['path'])) {
87 $path = $this->params['path'];
88 }
89  
90 if (!empty($this->params['connection'])) {
91 $connection = $this->params['connection'];
92 }
93 if (!empty($this->params['plugin'])) {
94 $plugin = $this->params['plugin'];
95 if (empty($name)) {
96 $name = $plugin;
97 }
98 }
99 $this->Schema = new CakeSchema(compact('name', 'path', 'file', 'connection', 'plugin'));
100 }
101  
102 /**
103 * Read and output contents of schema object
104 * path to read as second arg
105 *
106 * @return void
107 */
108 public function view() {
109 $File = new File($this->Schema->path . DS . $this->params['file']);
110 if ($File->exists()) {
111 $this->out($File->read());
112 $this->_stop();
113 } else {
114 $file = $this->Schema->path . DS . $this->params['file'];
115 $this->err(__d('cake_console', 'Schema file (%s) could not be found.', $file));
116 $this->_stop();
117 }
118 }
119  
120 /**
121 * Read database and Write schema object
122 * accepts a connection as first arg or path to save as second arg
123 *
124 * @return void
125 */
126 public function generate() {
127 $this->out(__d('cake_console', 'Generating Schema...'));
128 $options = array();
129 if ($this->params['force']) {
130 $options = array('models' => false);
131 }
132  
133 $snapshot = false;
134 if (isset($this->args[0]) && $this->args[0] === 'snapshot') {
135 $snapshot = true;
136 }
137  
138 if (!$snapshot && file_exists($this->Schema->path . DS . $this->params['file'])) {
139 $snapshot = true;
140 $prompt = __d('cake_console', "Schema file exists.\n [O]verwrite\n [S]napshot\n [Q]uit\nWould you like to do?");
141 $result = strtolower($this->in($prompt, array('o', 's', 'q'), 's'));
142 if ($result === 'q') {
143 return $this->_stop();
144 }
145 if ($result === 'o') {
146 $snapshot = false;
147 }
148 }
149  
150 $cacheDisable = Configure::read('Cache.disable');
151 Configure::write('Cache.disable', true);
152  
153 $content = $this->Schema->read($options);
154 $content['file'] = $this->params['file'];
155  
156 Configure::write('Cache.disable', $cacheDisable);
157  
158 if ($snapshot === true) {
159 $fileName = rtrim($this->params['file'], '.php');
160 $Folder = new Folder($this->Schema->path);
161 $result = $Folder->read();
162  
163 $numToUse = false;
164 if (isset($this->params['snapshot'])) {
165 $numToUse = $this->params['snapshot'];
166 }
167  
168 $count = 0;
169 if (!empty($result[1])) {
170 foreach ($result[1] as $file) {
171 if (preg_match('/' . preg_quote($fileName) . '(?:[_\d]*)?\.php$/', $file)) {
172 $count++;
173 }
174 }
175 }
176  
177 if ($numToUse !== false) {
178 if ($numToUse > $count) {
179 $count = $numToUse;
180 }
181 }
182  
183 $content['file'] = $fileName . '_' . $count . '.php';
184 }
185  
186 if ($this->Schema->write($content)) {
187 $this->out(__d('cake_console', 'Schema file: %s generated', $content['file']));
188 $this->_stop();
189 } else {
190 $this->err(__d('cake_console', 'Schema file: %s generated'));
191 $this->_stop();
192 }
193 }
194  
195 /**
196 * Dump Schema object to sql file
197 * Use the `write` param to enable and control SQL file output location.
198 * Simply using -write will write the sql file to the same dir as the schema file.
199 * If -write contains a full path name the file will be saved there. If -write only
200 * contains no DS, that will be used as the file name, in the same dir as the schema file.
201 *
202 * @return string
203 */
204 public function dump() {
205 $write = false;
206 $Schema = $this->Schema->load();
207 if (!$Schema) {
208 $this->err(__d('cake_console', 'Schema could not be loaded'));
209 $this->_stop();
210 }
211 if (!empty($this->params['write'])) {
212 if ($this->params['write'] == 1) {
213 $write = Inflector::underscore($this->Schema->name);
214 } else {
215 $write = $this->params['write'];
216 }
217 }
218 $db = ConnectionManager::getDataSource($this->Schema->connection);
219 $contents = "\n\n" . $db->dropSchema($Schema) . "\n\n" . $db->createSchema($Schema);
220  
221 if ($write) {
222 if (strpos($write, '.sql') === false) {
223 $write .= '.sql';
224 }
225 if (strpos($write, DS) !== false) {
226 $File = new File($write, true);
227 } else {
228 $File = new File($this->Schema->path . DS . $write, true);
229 }
230  
231 if ($File->write($contents)) {
232 $this->out(__d('cake_console', 'SQL dump file created in %s', $File->pwd()));
233 $this->_stop();
234 } else {
235 $this->err(__d('cake_console', 'SQL dump could not be created'));
236 $this->_stop();
237 }
238 }
239 $this->out($contents);
240 return $contents;
241 }
242  
243 /**
244 * Run database create commands. Alias for run create.
245 *
246 * @return void
247 */
248 public function create() {
249 list($Schema, $table) = $this->_loadSchema();
250 $this->_create($Schema, $table);
251 }
252  
253 /**
254 * Run database create commands. Alias for run create.
255 *
256 * @return void
257 */
258 public function update() {
259 list($Schema, $table) = $this->_loadSchema();
260 $this->_update($Schema, $table);
261 }
262  
263 /**
264 * Prepares the Schema objects for database operations.
265 *
266 * @return void
267 */
268 protected function _loadSchema() {
269 $name = $plugin = null;
270 if (!empty($this->params['name'])) {
271 $name = $this->params['name'];
272 }
273 if (!empty($this->params['plugin'])) {
274 $plugin = $this->params['plugin'];
275 }
276  
277 if (!empty($this->params['dry'])) {
278 $this->_dry = true;
279 $this->out(__d('cake_console', 'Performing a dry run.'));
280 }
281  
282 $options = array('name' => $name, 'plugin' => $plugin);
283 if (!empty($this->params['snapshot'])) {
284 $fileName = rtrim($this->Schema->file, '.php');
285 $options['file'] = $fileName . '_' . $this->params['snapshot'] . '.php';
286 }
287  
288 $Schema = $this->Schema->load($options);
289  
290 if (!$Schema) {
291 $this->err(__d('cake_console', '%s could not be loaded', $this->Schema->path . DS . $this->Schema->file));
292 $this->_stop();
293 }
294 $table = null;
295 if (isset($this->args[1])) {
296 $table = $this->args[1];
297 }
298 return array(&$Schema, $table);
299 }
300  
301 /**
302 * Create database from Schema object
303 * Should be called via the run method
304 *
305 * @param CakeSchema $Schema
306 * @param string $table
307 * @return void
308 */
309 protected function _create($Schema, $table = null) {
310 $db = ConnectionManager::getDataSource($this->Schema->connection);
311  
312 $drop = $create = array();
313  
314 if (!$table) {
315 foreach ($Schema->tables as $table => $fields) {
316 $drop[$table] = $db->dropSchema($Schema, $table);
317 $create[$table] = $db->createSchema($Schema, $table);
318 }
319 } elseif (isset($Schema->tables[$table])) {
320 $drop[$table] = $db->dropSchema($Schema, $table);
321 $create[$table] = $db->createSchema($Schema, $table);
322 }
323 if (empty($drop) || empty($create)) {
324 $this->out(__d('cake_console', 'Schema is up to date.'));
325 $this->_stop();
326 }
327  
328 $this->out("\n" . __d('cake_console', 'The following table(s) will be dropped.'));
329 $this->out(array_keys($drop));
330  
331 if ('y' == $this->in(__d('cake_console', 'Are you sure you want to drop the table(s)?'), array('y', 'n'), 'n')) {
332 $this->out(__d('cake_console', 'Dropping table(s).'));
333 $this->_run($drop, 'drop', $Schema);
334 }
335  
336 $this->out("\n" . __d('cake_console', 'The following table(s) will be created.'));
337 $this->out(array_keys($create));
338  
339 if ('y' == $this->in(__d('cake_console', 'Are you sure you want to create the table(s)?'), array('y', 'n'), 'y')) {
340 $this->out(__d('cake_console', 'Creating table(s).'));
341 $this->_run($create, 'create', $Schema);
342 }
343 $this->out(__d('cake_console', 'End create.'));
344 }
345  
346 /**
347 * Update database with Schema object
348 * Should be called via the run method
349 *
350 * @param CakeSchema $Schema
351 * @param string $table
352 * @return void
353 */
354 protected function _update(&$Schema, $table = null) {
355 $db = ConnectionManager::getDataSource($this->Schema->connection);
356  
357 $this->out(__d('cake_console', 'Comparing Database to Schema...'));
358 $options = array();
359 if (isset($this->params['force'])) {
360 $options['models'] = false;
361 }
362 $Old = $this->Schema->read($options);
363 $compare = $this->Schema->compare($Old, $Schema);
364  
365 $contents = array();
366  
367 if (empty($table)) {
368 foreach ($compare as $table => $changes) {
369 $contents[$table] = $db->alterSchema(array($table => $changes), $table);
370 }
371 } elseif (isset($compare[$table])) {
372 $contents[$table] = $db->alterSchema(array($table => $compare[$table]), $table);
373 }
374  
375 if (empty($contents)) {
376 $this->out(__d('cake_console', 'Schema is up to date.'));
377 $this->_stop();
378 }
379  
380 $this->out("\n" . __d('cake_console', 'The following statements will run.'));
381 $this->out(array_map('trim', $contents));
382 if ('y' == $this->in(__d('cake_console', 'Are you sure you want to alter the tables?'), array('y', 'n'), 'n')) {
383 $this->out();
384 $this->out(__d('cake_console', 'Updating Database...'));
385 $this->_run($contents, 'update', $Schema);
386 }
387  
388 $this->out(__d('cake_console', 'End update.'));
389 }
390  
391 /**
392 * Runs sql from _create() or _update()
393 *
394 * @param array $contents
395 * @param string $event
396 * @param CakeSchema $Schema
397 * @return void
398 */
399 protected function _run($contents, $event, &$Schema) {
400 if (empty($contents)) {
401 $this->err(__d('cake_console', 'Sql could not be run'));
402 return;
403 }
404 Configure::write('debug', 2);
405 $db = ConnectionManager::getDataSource($this->Schema->connection);
406  
407 foreach ($contents as $table => $sql) {
408 if (empty($sql)) {
409 $this->out(__d('cake_console', '%s is up to date.', $table));
410 } else {
411 if ($this->_dry === true) {
412 $this->out(__d('cake_console', 'Dry run for %s :', $table));
413 $this->out($sql);
414 } else {
415 if (!$Schema->before(array($event => $table))) {
416 return false;
417 }
418 $error = null;
419 try {
420 $db->execute($sql);
421 } catch (PDOException $e) {
422 $error = $table . ': ' . $e->getMessage();
423 }
424  
425 $Schema->after(array($event => $table, 'errors' => $error));
426  
427 if (!empty($error)) {
428 $this->err($error);
429 } else {
430 $this->out(__d('cake_console', '%s updated.', $table));
431 }
432 }
433 }
434 }
435 }
436  
437 /**
438 * get the option parser
439 *
440 * @return void
441 */
442 public function getOptionParser() {
443 $plugin = array(
444 'short' => 'p',
445 'help' => __d('cake_console', 'The plugin to use.'),
446 );
447 $connection = array(
448 'short' => 'c',
449 'help' => __d('cake_console', 'Set the db config to use.'),
450 'default' => 'default'
451 );
452 $path = array(
453 'help' => __d('cake_console', 'Path to read and write schema.php'),
454 'default' => APP . 'Config' . DS . 'Schema'
455 );
456 $file = array(
457 'help' => __d('cake_console', 'File name to read and write.'),
458 'default' => 'schema.php'
459 );
460 $name = array(
461 'help' => __d('cake_console', 'Classname to use. If its Plugin.class, both name and plugin options will be set.')
462 );
463 $snapshot = array(
464 'short' => 's',
465 'help' => __d('cake_console', 'Snapshot number to use/make.')
466 );
467 $dry = array(
468 'help' => __d('cake_console', 'Perform a dry run on create and update commands. Queries will be output instead of run.'),
469 'boolean' => true
470 );
471 $force = array(
472 'short' => 'f',
473 'help' => __d('cake_console', 'Force "generate" to create a new schema'),
474 'boolean' => true
475 );
476 $write = array(
477 'help' => __d('cake_console', 'Write the dumped SQL to a file.')
478 );
479  
480 $parser = parent::getOptionParser();
481 $parser->description(
482 __d('cake_console', 'The Schema Shell generates a schema object from the database and updates the database from the schema.')
483 )->addSubcommand('view', array(
484 'help' => __d('cake_console', 'Read and output the contents of a schema file'),
485 'parser' => array(
486 'options' => compact('plugin', 'path', 'file', 'name', 'connection'),
487 'arguments' => compact('name')
488 )
489 ))->addSubcommand('generate', array(
490 'help' => __d('cake_console', 'Reads from --connection and writes to --path. Generate snapshots with -s'),
491 'parser' => array(
492 'options' => compact('plugin', 'path', 'file', 'name', 'connection', 'snapshot', 'force'),
493 'arguments' => array(
494 'snapshot' => array('help' => __d('cake_console', 'Generate a snapshot.'))
495 )
496 )
497 ))->addSubcommand('dump', array(
498 'help' => __d('cake_console', 'Dump database SQL based on a schema file to stdout.'),
499 'parser' => array(
500 'options' => compact('plugin', 'path', 'file', 'name', 'connection', 'write'),
501 'arguments' => compact('name')
502 )
503 ))->addSubcommand('create', array(
504 'help' => __d('cake_console', 'Drop and create tables based on the schema file.'),
505 'parser' => array(
506 'options' => compact('plugin', 'path', 'file', 'name', 'connection', 'dry', 'snapshot'),
507 'args' => array(
508 'name' => array(
509 'help' => __d('cake_console', 'Name of schema to use.')
510 ),
511 'table' => array(
512 'help' => __d('cake_console', 'Only create the specified table.')
513 )
514 )
515 )
516 ))->addSubcommand('update', array(
517 'help' => __d('cake_console', 'Alter the tables based on the schema file.'),
518 'parser' => array(
519 'options' => compact('plugin', 'path', 'file', 'name', 'connection', 'dry', 'snapshot', 'force'),
520 'args' => array(
521 'name' => array(
522 'help' => __d('cake_console', 'Name of schema to use.')
523 ),
524 'table' => array(
525 'help' => __d('cake_console', 'Only create the specified table.')
526 )
527 )
528 )
529 ));
530 return $parser;
531 }
532  
533 }
534  
535