c32347006e18c76994ecef7c29e442c846fd24b4
[TYPO3CMS/Extensions/t3build.git] / provider / class.abstract.php
1 <?php
2 /**
3 * Base class for providers - which extracts the CLI help
4 * from the docBlocks of the class and the class vars which
5 * have an @arg tag.
6 *
7 * If you label an argument with @required it will be
8 * required and checked upfront - if it's missing, the
9 * execution will stop with an error.
10 *
11 * If you need wildcard arguments (eg. to pass them to
12 * another provider) you can label them with @mask:
13 * @mask --clean-*
14 *
15 * The type (@var) of the arguments will be considered and
16 * CLI args will be casted accordingly before execution.
17 *
18 * When there are setter methods for the arguments
19 * (setArgument) they will be called instead of directly
20 * setting the class vars.
21 *
22 * @package t3build
23 * @author Christian Opitz <co@netzelf.de>
24 */
25 abstract class tx_t3build_provider_abstract
26 {
27 /**
28 * Missing arguments
29 * @var array
30 */
31 private $_missing = array();
32
33 /**
34 * Argument information array
35 * @var array
36 */
37 private $_infos = array();
38
39 /**
40 * Required arguments
41 * @var array
42 */
43 private $_requireds = array();
44
45 /**
46 * Reflection of $this class
47 * @var ReflectionClass
48 */
49 private $_class;
50
51 /**
52 * Override this if you want the default action
53 * to be another than that with the class name
54 * + 'Action' as method name.
55 * @var string
56 */
57 protected $defaultActionName;
58
59 /**
60 * Print debug information
61 * @arg
62 * @var boolean
63 */
64 protected $debug = false;
65
66 /**
67 * Print help information
68 * @arg
69 * @var boolean
70 */
71 protected $help = false;
72
73 /**
74 * The raw cli args as passed from TYPO3
75 * @var array
76 */
77 protected $cliArgs = array();
78
79 /**
80 * Initialization: Retrieve the information about
81 * the arguments and set the corresponding class
82 * vars accordingly or fail the execution when
83 * @required arguments are missing.
84 *
85 * @param array $args
86 */
87 public function init($args)
88 {
89 $this->cliArgs = $args;
90 $this->_class = new ReflectionClass($this);
91 $masks = array();
92 $modifiers = array();
93
94 foreach ($this->_class->getProperties() as $i => $property) {
95 if (preg_match_all('/^\s+\*\s+@([^\s]+)(.*)$/m', $property->getDocComment(), $matches)) {
96 if (!in_array('arg', $matches[1])) {
97 continue;
98 }
99 $filteredName = ltrim($property->getName(), '_');
100 $name = ucfirst($filteredName);
101 preg_match_all('/[A-Z][a-z]*/', $name, $words);
102 $shorthand = '';
103 $switch = strtolower(implode('-', $words[0]));
104 $shorthand = !array_key_exists('-'.$filteredName[0], $modifiers) ? $filteredName[0] : null;
105 $info = array(
106 'setter' => method_exists($this, 'set'.$name) ? 'set'.$name : null,
107 'property' => $property->getName(),
108 'switch' => $switch,
109 'shorthand' => $shorthand,
110 'comment' => $property->getDocComment(),
111 'type' => null,
112 'mask' => null
113 );
114
115 $maskKey = array_search('mask', $matches[1]);
116 if ($maskKey !== false && $matches[2][$maskKey]) {
117 $info['type'] = 'mask';
118 $info['mask'] = ltrim(trim($matches[2][$maskKey]), '-');
119 $info['shorthand'] = $shorthand = null;
120 $info['switch'] = $switch = null;
121 $masks[$i] = trim($info['mask'], '*');
122 } else {
123 $varKey = array_search('var', $matches[1]);
124 if ($varKey !== false) {
125 $info['type'] = trim($matches[2][$varKey]);
126 }
127 }
128 $this->_infos[$i] = $info;
129 $this->_requireds[$i] = in_array('required', $matches[1]);
130 if ($shorthand) {
131 $modifiers['-'.$shorthand] = $i;
132 }
133 if ($switch) {
134 $modifiers['--'.$switch] = $i;
135 }
136 }
137 }
138 $values = array();
139 foreach ($args as $argument => $value) {
140 if (!preg_match('/^(-{1,2})(.+)/', $argument, $parts)) {
141 continue;
142 }
143 $realArgs = array($parts[2]);
144 $argsCount = 1;
145 for ($n = 0; $n < $argsCount; $n++) {
146 $modifier = $parts[1].$realArgs[$n];
147 if (!array_key_exists($modifier, $modifiers)) {
148 if ($argsCount == 1) {
149 foreach ($masks as $i => $mask) {
150 if (substr($parts[2], 0, $l = strlen($mask)) == $mask) {
151 if (!isset($values[$i])) {
152 $values[$i] = (array) $this->{$this->_infos[$i]['property']};
153 }
154 $values[$i][$parts[1].substr($parts[2], $l)] = $value;
155 break 2;
156 }
157 }
158 if ($parts[1] == '-') {
159 $realArgs = str_split('0'.$parts[2]);
160 $argsCount = count($realArgs);
161 continue;
162 }
163 }
164 $this->_die('Unknown modifier "%s"', $modifier);
165 }
166 $i = $modifiers[$modifier];
167 switch ($this->_infos[$i]['type']) {
168 case 'boolean':
169 case 'bool':
170 $value = !count($value) || !in_array($value[0], array('false', '0'), true) ? true : false;
171 break;
172 case 'string':
173 $value = implode(',', $value);
174 break;
175 case 'int':
176 case 'integer':
177 $value = (int) $value[0];
178 break;
179 case 'float':
180 $value = (float) $value[0];
181 break;
182 case 'array':
183 break;
184 default:
185 $value = $value[0];
186 }
187 if ($this->_infos[$i]['property'] == 'debug') {
188 $this->debug = $value;
189 }
190 $values[$i] = $value;
191 }
192 }
193 foreach ($values as $i => $value) {
194 if ($this->_infos[$i]['setter']) {
195 $this->_debug('Calling setter '.$this->_infos[$i]['setter'].' with ', $value);
196 $this->{$this->_infos[$i]['setter']}($value);
197 } else {
198 $this->_debug('Setting property '.$this->_infos[$i]['property'].' to ', $value);
199 $this->{$this->_infos[$i]['property']} = $value;
200 }
201 }
202 foreach ($this->_requireds as $i => $required) {
203 if ($required && !array_key_exists($i, $values)) {
204 $this->_missing[] = '"'.$this->_infos[$i]['switch'].'"';
205 }
206 }
207 }
208
209 /**
210 * Render the help from the argument information
211 * @return string
212 */
213 protected function renderHelp()
214 {
215 preg_match_all('/^\s+\* ([^@\/].*)$/m', $this->_class->getDocComment(), $lines);
216 $help = implode("\n", $lines[1])."\n\n";
217 $help .= 'php '.$_SERVER['PHP_SELF'];
218 foreach ($this->_requireds as $i => $required) {
219 if ($required) {
220 $help .= ' -'.$this->_infos[$i]['shorthand'].' "'.$this->_infos[$i]['switch'].'"';
221 }
222 }
223
224 $longest = 0;
225 $order = array();
226 foreach ($this->_infos as $i => $info) {
227 // Help stuff
228 preg_match_all('/^\s+\* ([^@\/].*)$/m', $info['comment'], $lines);
229 $this->_infos[$i]['desc'] = $lines[1];
230 $this->_infos[$i]['default'] = $this->{$info['property']};
231 if ($this->_infos[$i]['mask']) {
232 $this->_infos[$i]['switchDesc'] = '-'.$this->_infos[$i]['mask'].', --'.$this->_infos[$i]['mask'];
233 } else {
234 $this->_infos[$i]['switchDesc'] = '--'.$info['switch'];
235 if ($info['shorthand']) {
236 $this->_infos[$i]['switchDesc'] = '-'.$info['shorthand'].' ['.$this->_infos[$i]['switchDesc'].']';
237 }
238 }
239 $length = strlen($this->_infos[$i]['switchDesc']);
240 if ($length > $longest) {
241 $longest = $length;
242 }
243 $order[$i] = $info['switch'];
244 }
245
246 asort($order);
247
248 $help .= PHP_EOL.PHP_EOL;
249 $pre = str_repeat(' ', $longest+1);
250 foreach (array_keys($order) as $i) {
251 $info = $this->_infos[$i];
252 $length = strlen($info['switchDesc']);
253 $default = $info['default'];
254 if ($default !== '' && $default !== null) {
255 if ($default === true) {
256 $default = 'true';
257 } elseif ($default === false) {
258 $default = 'false';
259 } elseif ($info['type'] == 'array') {
260 $default = implode(', ', (array) $default);
261 }
262 $info['desc'][] .= '(defaults to "'.$default.'")';
263 }
264 $help .= $info['switchDesc'].str_repeat(' ', $longest - $length + 1).':'.' ';
265 $help .= implode(PHP_EOL.str_repeat(' ', $longest+3), $info['desc']);
266 $help .= PHP_EOL;
267 }
268
269 return $help;
270 }
271
272 /**
273 * Output help
274 */
275 public function helpAction()
276 {
277 $this->_echo($this->renderHelp());
278 }
279
280 /**
281 * Run the provider
282 *
283 * @param string|null $action
284 * @return mixed|void
285 */
286 public function run($action = null)
287 {
288 if ($this->help) {
289 $action = 'help';
290 }
291 if (!$action) {
292 if ($this->defaultActionName) {
293 $action = $this->defaultActionName;
294 } else {
295 $methods = $this->_class->getMethods();
296 $actions = array();
297 foreach ($methods as $method) {
298 /* @var $method ReflectionMethod */
299 if ($method->name != 'helpAction' && substr($method->name, -6) == 'Action') {
300 $actions[$name = substr($method->name, 0, -6)] = $name;
301 }
302 }
303 if (count($actions) == 1) {
304 $action = array_shift($actions);
305 }
306 }
307 }
308 if (!$action) {
309 $this->_echo('No action provided');
310 $action = 'help';
311 }
312 if (!is_callable(array($this, $action.'Action'))) {
313 $this->_echo('Invalid action "'.$action.'"');
314 $action = 'help';
315 }
316 if (count($this->_missing) && $action != 'help') {
317 $this->_echo('Missing argument'.(count($this->_missing) > 1 ? 's' : '').' %s', $this->_missing);
318 $action = 'help';
319 }
320 return call_user_func(array($this, $action.'Action'));
321 }
322
323 /**
324 * Echo vsprintfed string
325 *
326 * @param string $msg (can contain sprintf format)
327 * @param mixed $arg
328 * @param ...
329 */
330 protected function _echo($msg)
331 {
332 $args = func_get_args();
333 array_shift($args);
334 foreach ($args as $i => $arg) {
335 if (is_array($arg)) {
336 $and = is_numeric($i) ? 'and' : $i;
337 $last = array_pop($arg);
338 $args[$i] = count($arg) ? implode(', ', $arg).' '.$and.' '.$last : $last;
339 }
340 }
341 echo vsprintf((string) $msg, $args)."\n";
342 }
343
344 /**
345 * Echo vsprintfed string and exit with error
346 *
347 * @param string $msg (can contain sprintf format)
348 * @param mixed $arg
349 * @param ...
350 */
351 protected function _die($msg)
352 {
353 $args = func_get_args();
354 call_user_func_array(array($this, '_echo'), $args);
355 exit(1);
356 }
357
358 /**
359 * Dump vars only if --debug is on
360 *
361 * @param string $msg
362 * @param mixed $var
363 * @param ...
364 */
365 protected function _debug($msg)
366 {
367 if (!$this->debug) {
368 return;
369 }
370 $args = func_get_args();
371 echo '[Debug] '.trim(array_shift($args));
372 if (count($args)) {
373 echo ' ';
374 call_user_func_array('var_dump', $args);
375 } else {
376 echo PHP_EOL;
377 }
378 }
379
380 /**
381 * Write config to extConf
382 *
383 * @param string $extKey
384 * @param array $update
385 */
386 protected function writeExtConf($extKey, array $update)
387 {
388 global $TYPO3_CONF_VARS;
389
390 $absPath = t3lib_extMgm::extPath($extKey);
391 $relPath = t3lib_extMgm::extRelPath($extKey);
392
393 /* @var $tsStyleConfig t3lib_tsStyleConfig */
394 $tsStyleConfig = t3lib_div::makeInstance('t3lib_tsStyleConfig');
395 $theConstants = $tsStyleConfig->ext_initTSstyleConfig(
396 t3lib_div::getUrl($absPath . 'ext_conf_template.txt'),
397 $absPath,
398 $relPath,
399 ''
400 );
401
402 $arr = @unserialize($TYPO3_CONF_VARS['EXT']['extConf'][$extKey]);
403 $arr = is_array($arr) ? $arr : array();
404
405 // Call processing function for constants config and data before write and form rendering:
406 if (is_array($TYPO3_CONF_VARS['SC_OPTIONS']['typo3/mod/tools/em/index.php']['tsStyleConfigForm'])) {
407 $_params = array('fields' => &$theConstants, 'data' => &$arr, 'extKey' => $extKey);
408 foreach ($TYPO3_CONF_VARS['SC_OPTIONS']['typo3/mod/tools/em/index.php']['tsStyleConfigForm'] as $_funcRef) {
409 t3lib_div::callUserFunction($_funcRef, $_params, $this);
410 }
411 unset($_params);
412 }
413
414
415 $arr = t3lib_div::array_merge_recursive_overrule($arr, $update);
416
417 /* @var $instObj t3lib_install */
418 $instObj = t3lib_div::makeInstance('t3lib_install');
419 $instObj->allowUpdateLocalConf = 1;
420 $instObj->updateIdentity = 'TYPO3 Extension Manager';
421
422 // Get lines from localconf file
423 $lines = $instObj->writeToLocalconf_control();
424 $instObj->setValueInLocalconfFile($lines, '$TYPO3_CONF_VARS[\'EXT\'][\'extConf\'][\'' . $extKey . '\']', serialize($arr)); // This will be saved only if there are no linebreaks in it !
425 $instObj->writeToLocalconf_control($lines);
426
427 t3lib_extMgm::removeCacheFiles();
428 }
429
430 /**
431 * Parses $vars into a path mask and makes it FS-safe
432 *
433 * @param string $mask
434 * @param array $vars
435 * @param string $renameMode
436 * @param boolean $absolute
437 * @return string
438 */
439 protected function getPath($mask, $vars, $renameMode = 'camelCase', $absolute = false)
440 {
441 $replace = array();
442 foreach ($vars as $key => $value) {
443 $replace[] = '${'.$key.'}';
444 }
445 $path = str_replace($replace, $vars, $mask);
446 if (preg_match('/\$\{([^\}]*)\}/', $path, $res)) {
447 $this->_die('Unknown var "'.$res[1].'" in path mask');
448 }
449
450 $pre = '';
451 if ($absolute) {
452 $parts = preg_split('#\s*[\\/]+\s*#', $path);
453 $rest = array();
454 while (count($parts)) {
455 $file = implode('/', $parts);
456 if (file_exists($file)) {
457 if (!count($rest) && is_file($file)) {
458 return $file;
459 }
460 $pre = $file.'/';
461 $path = implode('/', $rest);
462 break;
463 }
464 array_unshift($rest, array_pop($parts));
465 }
466 }
467
468 $path = strtolower($path);
469 $path = str_replace(':', '-', $path);
470 $path = preg_replace('#[^A-Za-z0-9/\-_\.]+#', ' ', $path);
471 $path = preg_replace('#\s*/+\s*#', '/', $path);
472 $parts = explode(' ', $path);
473 if ($renameMode == 'underscore') {
474 $path = implode('_', $parts);
475 } else {
476 $path = '';
477 $uc = false;
478 foreach ($parts as $part) {
479 $ucPart = ucfirst($part);
480 $path .= ($uc || $renameMode === 'CamelCase') ? $ucPart : $part;
481 $uc = $ucPart != $part;
482 }
483 }
484 return $pre.$path;
485 }
486 }