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: $parser = parent::getOptionParser();
114:
115: $parser->description(array(
116: 'The interactive console is a tool for testing parts of your',
117: 'app before you write code.',
118: '',
119: 'See below for a list of supported commands.'
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:
181: return $parser;
182: }
183: 184: 185: 186: 187:
188: public function help() {
189: $optionParser = $this->getOptionParser();
190: $this->out($optionParser->epilog());
191: }
192:
193: 194: 195: 196: 197: 198:
199: public function main($command = null) {
200: $this->_finished = false;
201: while (!$this->_finished) {
202: if (empty($command)) {
203: $command = trim($this->in(''));
204: }
205:
206: $method = $this->_method($command);
207:
208: if ($method) {
209: $this->$method($command);
210: } else {
211: $this->out(__d('cake_console', "Invalid command"));
212: $this->out();
213: }
214: $command = '';
215: }
216: }
217:
218: 219: 220: 221: 222: 223:
224: protected function _method($command) {
225: foreach ($this->_methodPatterns as $method => $pattern) {
226: if (preg_match($pattern, $command)) {
227: return $method;
228: }
229: }
230:
231: return false;
232: }
233:
234: 235: 236: 237: 238:
239: protected function _exit() {
240: $this->_finished = true;
241: }
242:
243: 244: 245: 246: 247:
248: protected function _models() {
249: $this->out(__d('cake_console', 'Model classes:'));
250: $this->hr();
251: foreach ($this->models as $model) {
252: $this->out(" - {$model}");
253: }
254: }
255:
256: 257: 258: 259: 260: 261:
262: protected function _bind($command) {
263: preg_match($this->_methodPatterns[__FUNCTION__], $command, $tmp);
264:
265: foreach ($tmp as $data) {
266: $data = strip_tags($data);
267: $data = str_replace($this->badCommandChars, "", $data);
268: }
269:
270: $modelA = $tmp[1];
271: $association = $tmp[2];
272: $modelB = $tmp[3];
273:
274: if ($this->_isValidModel($modelA) && $this->_isValidModel($modelB) && in_array($association, $this->associations)) {
275: $this->{$modelA}->bindModel(array($association => array($modelB => array('className' => $modelB))), false);
276: $this->out(__d('cake_console', "Created %s association between %s and %s",
277: $association, $modelA, $modelB));
278: } else {
279: $this->out(__d('cake_console', "Please verify you are using valid models and association types"));
280: }
281: }
282:
283: 284: 285: 286: 287: 288:
289: protected function _unbind($command) {
290: preg_match($this->_methodPatterns[__FUNCTION__], $command, $tmp);
291:
292: foreach ($tmp as $data) {
293: $data = strip_tags($data);
294: $data = str_replace($this->badCommandChars, "", $data);
295: }
296:
297: $modelA = $tmp[1];
298: $association = $tmp[2];
299: $modelB = $tmp[3];
300:
301:
302: $currentAssociations = $this->{$modelA}->getAssociated();
303: $validCurrentAssociation = false;
304:
305: foreach ($currentAssociations as $model => $currentAssociation) {
306: if ($model === $modelB && $association === $currentAssociation) {
307: $validCurrentAssociation = true;
308: }
309: }
310:
311: if ($this->_isValidModel($modelA) && $this->_isValidModel($modelB) && in_array($association, $this->associations) && $validCurrentAssociation) {
312: $this->{$modelA}->unbindModel(array($association => array($modelB)));
313: $this->out(__d('cake_console', "Removed %s association between %s and %s",
314: $association, $modelA, $modelB));
315: } else {
316: $this->out(__d('cake_console', "Please verify you are using valid models, valid current association, and valid association types"));
317: }
318: }
319:
320: 321: 322: 323: 324: 325:
326: protected function _find($command) {
327: $command = strip_tags($command);
328: $command = str_replace($this->badCommandChars, "", $command);
329:
330:
331: list($modelToCheck) = explode('->', $command);
332:
333: if ($this->_isValidModel($modelToCheck)) {
334: $findCommand = "\$data = \$this->$command;";
335:
336: @eval($findCommand);
337:
338:
339: if (is_array($data)) {
340: foreach ($data as $idx => $results) {
341: if (is_numeric($idx)) {
342: foreach ($results as $modelName => $result) {
343: $this->out("$modelName");
344:
345: foreach ($result as $field => $value) {
346: if (is_array($value)) {
347: foreach ($value as $field2 => $value2) {
348: $this->out("\t$field2: $value2");
349: }
350:
351: $this->out();
352: } else {
353: $this->out("\t$field: $value");
354: }
355: }
356: }
357: } else {
358: $this->out($idx);
359:
360: foreach ($results as $field => $value) {
361: if (is_array($value)) {
362: foreach ($value as $field2 => $value2) {
363: $this->out("\t$field2: $value2");
364: }
365:
366: $this->out();
367: } else {
368: $this->out("\t$field: $value");
369: }
370: }
371: }
372: }
373: } else {
374: $this->out();
375: $this->out(__d('cake_console', "No result set found"));
376: }
377: } else {
378: $this->out(__d('cake_console', "%s is not a valid model", $modelToCheck));
379: }
380: }
381:
382: 383: 384: 385: 386: 387:
388: protected function _save($command) {
389:
390: $command = strip_tags($command);
391: $command = str_replace($this->badCommandChars, "", $command);
392: list($modelToSave) = explode("->", $command);
393:
394: if ($this->_isValidModel($modelToSave)) {
395:
396: list(, $data) = explode("->save", $command);
397: $data = preg_replace('/^\(*(array)?\(*(.+?)\)*$/i', '\\2', $data);
398: $saveCommand = "\$this->{$modelToSave}->save(array('{$modelToSave}' => array({$data})));";
399:
400: @eval($saveCommand);
401:
402: $this->out(__d('cake_console', 'Saved record for %s', $modelToSave));
403: }
404: }
405:
406: 407: 408: 409: 410: 411:
412: protected function _columns($command) {
413: preg_match($this->_methodPatterns[__FUNCTION__], $command, $tmp);
414:
415: $modelToCheck = strip_tags(str_replace($this->badCommandChars, "", $tmp[1]));
416:
417: if ($this->_isValidModel($modelToCheck)) {
418:
419: $fieldsCommand = "\$data = \$this->{$modelToCheck}->getColumnTypes();";
420:
421: @eval($fieldsCommand);
422:
423:
424: if (is_array($data)) {
425: foreach ($data as $field => $type) {
426: $this->out("\t{$field}: {$type}");
427: }
428: }
429: } else {
430: $this->out(__d('cake_console', "Please verify that you selected a valid model"));
431: }
432: }
433:
434: 435: 436: 437: 438:
439: protected function _routesReload() {
440: if (!$this->_loadRoutes()) {
441: 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."));
442: }
443: $this->out(__d('cake_console', "Routes configuration reloaded, %d routes connected", count(Router::$routes)));
444: }
445:
446: 447: 448: 449: 450:
451: protected function _routesShow() {
452: $this->out(print_r(Hash::combine(Router::$routes, '{n}.template', '{n}.defaults'), true));
453: }
454:
455: 456: 457: 458: 459: 460:
461: protected function _routeToString($command) {
462: preg_match($this->_methodPatterns[__FUNCTION__], $command, $tmp);
463:
464:
465: if ($url = eval('return array' . $tmp[1] . ';')) {
466:
467: $this->out(Router::url($url));
468: }
469: }
470:
471: 472: 473: 474: 475: 476:
477: protected function _routeToArray($command) {
478: preg_match($this->_methodPatterns[__FUNCTION__], $command, $tmp);
479:
480: $this->out(var_export(Router::parse($tmp[1]), true));
481: }
482:
483: 484: 485: 486: 487: 488:
489: protected function _isValidModel($modelToCheck) {
490: return in_array($modelToCheck, $this->models);
491: }
492:
493: 494: 495: 496: 497: 498:
499: protected function _loadRoutes() {
500: Router::reload();
501: extract(Router::getNamedExpressions());
502:
503:
504: if (!@include APP . 'Config' . DS . 'routes.php') {
505:
506: return false;
507: }
508: CakePlugin::routes();
509:
510: Router::parse('/');
511: return true;
512: }
513:
514: }
515: