1: <?php
2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14:
15:
16: App::uses('AppShell', 'Console/Command');
17:
18: 19: 20: 21: 22: 23:
24: class ConsoleShell extends AppShell {
25:
26: 27: 28: 29: 30:
31: public $associations = array('hasOne', 'hasMany', 'belongsTo', 'hasAndBelongsToMany');
32:
33: 34: 35: 36: 37:
38: public $badCommandChars = array('$', ';');
39:
40: 41: 42: 43: 44:
45: public $models = array();
46:
47: 48: 49: 50: 51: 52: 53:
54: protected $_finished = false;
55:
56: 57: 58: 59: 60:
61: protected $_methodPatterns = array(
62: 'help' => '/^(help|\?)/',
63: '_exit' => '/^(quit|exit)/',
64: '_models' => '/^models/i',
65: '_bind' => '/^(\w+) bind (\w+) (\w+)/',
66: '_unbind' => '/^(\w+) unbind (\w+) (\w+)/',
67: '_find' => '/.+->find/',
68: '_save' => '/.+->save/',
69: '_columns' => '/^(\w+) columns/',
70: '_routesReload' => '/^routes\s+reload/i',
71: '_routesShow' => '/^routes\s+show/i',
72: '_routeToString' => '/^route\s+(\(.*\))$/i',
73: '_routeToArray' => '/^route\s+(.*)$/i',
74: );
75:
76: 77: 78: 79: 80:
81: public function startup() {
82: App::uses('Dispatcher', 'Routing');
83: $this->Dispatcher = new Dispatcher();
84: $this->models = App::objects('Model');
85:
86: foreach ($this->models as $model) {
87: $class = $model;
88: App::uses($class, 'Model');
89: $this->{$class} = new $class();
90: }
91: $this->out(__d('cake_console', 'Model classes:'));
92: $this->hr();
93:
94: foreach ($this->models as $model) {
95: $this->out(" - {$model}");
96: }
97:
98: if (!$this->_loadRoutes()) {
99: $message = __d(
100: 'cake_console',
101: 'There was an error loading the routes config. Please check that the file exists and contains no errors.'
102: );
103: $this->err($message);
104: }
105: }
106:
107: 108: 109: 110: 111:
112: public function getOptionParser() {
113: $description = array(
114: 'The interactive console is a tool for testing parts of your',
115: 'app before you write code.',
116: '',
117: 'See below for a list of supported commands.'
118: );
119:
120: $epilog = array(
121: '<info>Model testing</info>',
122: '',
123: 'To test model results, use the name of your model without a leading $',
124: 'e.g. Foo->find("all")',
125: "",
126: 'To dynamically set associations, you can do the following:',
127: '',
128: "\tModelA bind <association> ModelB",
129: '',
130: "where the supported associations are hasOne, hasMany, belongsTo, hasAndBelongsToMany",
131: "",
132: 'To dynamically remove associations, you can do the following:',
133: '',
134: "\t ModelA unbind <association> ModelB",
135: '',
136: "where the supported associations are the same as above",
137: "",
138: "To save a new field in a model, you can do the following:",
139: '',
140: "\tModelA->save(array('foo' => 'bar', 'baz' => 0))",
141: '',
142: "where you are passing a hash of data to be saved in the format",
143: "of field => value pairs",
144: "",
145: "To get column information for a model, use the following:",
146: '',
147: "\tModelA columns",
148: '',
149: "which returns a list of columns and their type",
150: "",
151: '<info>Route testing</info>',
152: "",
153: 'To test URLs against your app\'s route configuration, type:',
154: "",
155: "\tRoute <url>",
156: "",
157: "where url is the path to your your action plus any query parameters,",
158: "minus the application's base path. For example:",
159: "",
160: "\tRoute /posts/view/1",
161: "",
162: "will return something like the following:",
163: "",
164: "\tarray(",
165: "\t [...]",
166: "\t 'controller' => 'posts',",
167: "\t 'action' => 'view',",
168: "\t [...]",
169: "\t)",
170: "",
171: 'Alternatively, you can use simple array syntax to test reverse',
172: 'To reload your routes config (Config/routes.php), do the following:',
173: "",
174: "\tRoutes reload",
175: "",
176: 'To show all connected routes, do the following:',
177: '',
178: "\tRoutes show",
179: );
180: return parent::getOptionParser()
181: ->description($description)
182: ->epilog($epilog);
183: }
184: 185: 186: 187: 188:
189: public function help() {
190: $optionParser = $this->getOptionParser();
191: $this->out($optionParser->epilog());
192: }
193:
194: 195: 196: 197: 198: 199:
200: public function main($command = null) {
201: $this->_finished = false;
202: while (!$this->_finished) {
203: if (empty($command)) {
204: $command = trim($this->in(''));
205: }
206:
207: $method = $this->_method($command);
208:
209: if ($method) {
210: $this->$method($command);
211: } else {
212: $this->out(__d('cake_console', "Invalid command"));
213: $this->out();
214: }
215: $command = '';
216: }
217: }
218:
219: 220: 221: 222: 223: 224:
225: protected function _method($command) {
226: foreach ($this->_methodPatterns as $method => $pattern) {
227: if (preg_match($pattern, $command)) {
228: return $method;
229: }
230: }
231:
232: return false;
233: }
234:
235: 236: 237: 238: 239:
240: protected function _exit() {
241: $this->_finished = true;
242: }
243:
244: 245: 246: 247: 248:
249: protected function _models() {
250: $this->out(__d('cake_console', 'Model classes:'));
251: $this->hr();
252: foreach ($this->models as $model) {
253: $this->out(" - {$model}");
254: }
255: }
256:
257: 258: 259: 260: 261: 262:
263: protected function _bind($command) {
264: preg_match($this->_methodPatterns[__FUNCTION__], $command, $tmp);
265:
266: foreach ($tmp as $data) {
267: $data = strip_tags($data);
268: $data = str_replace($this->badCommandChars, "", $data);
269: }
270:
271: $modelA = $tmp[1];
272: $association = $tmp[2];
273: $modelB = $tmp[3];
274:
275: if ($this->_isValidModel($modelA) && $this->_isValidModel($modelB) && in_array($association, $this->associations)) {
276: $this->{$modelA}->bindModel(array($association => array($modelB => array('className' => $modelB))), false);
277: $this->out(__d('cake_console', "Created %s association between %s and %s",
278: $association, $modelA, $modelB));
279: } else {
280: $this->out(__d('cake_console', "Please verify you are using valid models and association types"));
281: }
282: }
283:
284: 285: 286: 287: 288: 289:
290: protected function _unbind($command) {
291: preg_match($this->_methodPatterns[__FUNCTION__], $command, $tmp);
292:
293: foreach ($tmp as $data) {
294: $data = strip_tags($data);
295: $data = str_replace($this->badCommandChars, "", $data);
296: }
297:
298: $modelA = $tmp[1];
299: $association = $tmp[2];
300: $modelB = $tmp[3];
301:
302:
303: $currentAssociations = $this->{$modelA}->getAssociated();
304: $validCurrentAssociation = false;
305:
306: foreach ($currentAssociations as $model => $currentAssociation) {
307: if ($model === $modelB && $association === $currentAssociation) {
308: $validCurrentAssociation = true;
309: }
310: }
311:
312: if ($this->_isValidModel($modelA) && $this->_isValidModel($modelB) && in_array($association, $this->associations) && $validCurrentAssociation) {
313: $this->{$modelA}->unbindModel(array($association => array($modelB)));
314: $this->out(__d('cake_console', "Removed %s association between %s and %s",
315: $association, $modelA, $modelB));
316: } else {
317: $this->out(__d('cake_console', "Please verify you are using valid models, valid current association, and valid association types"));
318: }
319: }
320:
321: 322: 323: 324: 325: 326:
327: protected function _find($command) {
328: $command = strip_tags($command);
329: $command = str_replace($this->badCommandChars, "", $command);
330:
331:
332: list($modelToCheck) = explode('->', $command);
333:
334: if ($this->_isValidModel($modelToCheck)) {
335: $findCommand = "\$data = \$this->$command;";
336:
337: @eval($findCommand);
338:
339:
340: if (is_array($data)) {
341: foreach ($data as $idx => $results) {
342: if (is_numeric($idx)) {
343: foreach ($results as $modelName => $result) {
344: $this->out("$modelName");
345:
346: foreach ($result as $field => $value) {
347: if (is_array($value)) {
348: foreach ($value as $field2 => $value2) {
349: $this->out("\t$field2: $value2");
350: }
351:
352: $this->out();
353: } else {
354: $this->out("\t$field: $value");
355: }
356: }
357: }
358: } else {
359: $this->out($idx);
360:
361: foreach ($results as $field => $value) {
362: if (is_array($value)) {
363: foreach ($value as $field2 => $value2) {
364: $this->out("\t$field2: $value2");
365: }
366:
367: $this->out();
368: } else {
369: $this->out("\t$field: $value");
370: }
371: }
372: }
373: }
374: } else {
375: $this->out();
376: $this->out(__d('cake_console', "No result set found"));
377: }
378: } else {
379: $this->out(__d('cake_console', "%s is not a valid model", $modelToCheck));
380: }
381: }
382:
383: 384: 385: 386: 387: 388:
389: protected function _save($command) {
390:
391: $command = strip_tags($command);
392: $command = str_replace($this->badCommandChars, "", $command);
393: list($modelToSave) = explode("->", $command);
394:
395: if ($this->_isValidModel($modelToSave)) {
396:
397: list(, $data) = explode("->save", $command);
398: $data = preg_replace('/^\(*(array)?\(*(.+?)\)*$/i', '\\2', $data);
399: $saveCommand = "\$this->{$modelToSave}->save(array('{$modelToSave}' => array({$data})));";
400:
401: @eval($saveCommand);
402:
403: $this->out(__d('cake_console', 'Saved record for %s', $modelToSave));
404: }
405: }
406:
407: 408: 409: 410: 411: 412:
413: protected function _columns($command) {
414: preg_match($this->_methodPatterns[__FUNCTION__], $command, $tmp);
415:
416: $modelToCheck = strip_tags(str_replace($this->badCommandChars, "", $tmp[1]));
417:
418: if ($this->_isValidModel($modelToCheck)) {
419:
420: $fieldsCommand = "\$data = \$this->{$modelToCheck}->getColumnTypes();";
421:
422: @eval($fieldsCommand);
423:
424:
425: if (is_array($data)) {
426: foreach ($data as $field => $type) {
427: $this->out("\t{$field}: {$type}");
428: }
429: }
430: } else {
431: $this->out(__d('cake_console', "Please verify that you selected a valid model"));
432: }
433: }
434:
435: 436: 437: 438: 439:
440: protected function _routesReload() {
441: if (!$this->_loadRoutes()) {
442: return $this->err(__d('cake_console', "There was an error loading the routes config. Please check that the file exists and is free of parse errors."));
443: }
444: $this->out(__d('cake_console', "Routes configuration reloaded, %d routes connected", count(Router::$routes)));
445: }
446:
447: 448: 449: 450: 451:
452: protected function _routesShow() {
453: $this->out(print_r(Hash::combine(Router::$routes, '{n}.template', '{n}.defaults'), true));
454: }
455:
456: 457: 458: 459: 460: 461:
462: protected function _routeToString($command) {
463: preg_match($this->_methodPatterns[__FUNCTION__], $command, $tmp);
464:
465:
466: if ($url = eval('return array' . $tmp[1] . ';')) {
467:
468: $this->out(Router::url($url));
469: }
470: }
471:
472: 473: 474: 475: 476: 477:
478: protected function _routeToArray($command) {
479: preg_match($this->_methodPatterns[__FUNCTION__], $command, $tmp);
480:
481: $this->out(var_export(Router::parse($tmp[1]), true));
482: }
483:
484: 485: 486: 487: 488: 489:
490: protected function _isValidModel($modelToCheck) {
491: return in_array($modelToCheck, $this->models);
492: }
493:
494: 495: 496: 497: 498: 499:
500: protected function _loadRoutes() {
501: Router::reload();
502: extract(Router::getNamedExpressions());
503:
504:
505: if (!@include APP . 'Config' . DS . 'routes.php') {
506:
507: return false;
508: }
509: CakePlugin::routes();
510:
511: Router::parse('/');
512: return true;
513: }
514:
515: }
516: