Completed
Push — master ( 7a5d54...ee8e05 )
by Greg
02:43
created

CommandInfo::hasAnnotation()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 5
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 3
nc 1
nop 1
1
<?php
2
namespace Consolidation\AnnotatedCommand\Parser;
3
4
use Symfony\Component\Console\Input\InputOption;
5
use Consolidation\AnnotatedCommand\Parser\Internal\CommandDocBlockParser;
6
use Consolidation\AnnotatedCommand\Parser\Internal\CommandDocBlockParserFactory;
7
use Consolidation\AnnotatedCommand\AnnotationData;
8
9
/**
10
 * Given a class and method name, parse the annotations in the
11
 * DocBlock comment, and provide accessor methods for all of
12
 * the elements that are needed to create a Symfony Console Command.
13
 *
14
 * Note that the name of this class is now somewhat of a misnomer,
15
 * as we now use it to hold annotation data for hooks as well as commands.
16
 * It would probably be better to rename this to MethodInfo at some point.
17
 */
18
class CommandInfo
19
{
20
    /**
21
     * Serialization schema version. Incremented every time the serialization schema changes.
22
     */
23
    const SERIALIZATION_SCHEMA_VERSION = 1;
24
25
    /**
26
     * @var \ReflectionMethod
27
     */
28
    protected $reflection;
29
30
    /**
31
     * @var boolean
32
     * @var string
33
    */
34
    protected $docBlockIsParsed = false;
35
36
    /**
37
     * @var string
38
     */
39
    protected $name;
40
41
    /**
42
     * @var string
43
     */
44
    protected $description = '';
45
46
    /**
47
     * @var string
48
     */
49
    protected $help = '';
50
51
    /**
52
     * @var DefaultsWithDescriptions
53
     */
54
    protected $options;
55
56
    /**
57
     * @var DefaultsWithDescriptions
58
     */
59
    protected $arguments;
60
61
    /**
62
     * @var array
63
     */
64
    protected $exampleUsage = [];
65
66
    /**
67
     * @var AnnotationData
68
     */
69
    protected $otherAnnotations;
70
71
    /**
72
     * @var array
73
     */
74
    protected $aliases = [];
75
76
    /**
77
     * @var InputOption[]
78
     */
79
    protected $inputOptions;
80
81
    /**
82
     * @var string
83
     */
84
    protected $methodName;
85
86
    /**
87
     * @var string
88
     */
89
    protected $returnType;
90
91
    /**
92
     * Create a new CommandInfo class for a particular method of a class.
93
     *
94
     * @param string|mixed $classNameOrInstance The name of a class, or an
95
     *   instance of it, or an array of cached data.
96
     * @param string $methodName The name of the method to get info about.
97
     * @param array $cache Cached data
98
     * @deprecated Use CommandInfo::create() or CommandInfo::deserialize()
99
     *   instead. In the future, this constructor will be protected.
100
     */
101
    public function __construct($classNameOrInstance, $methodName, $cache = [])
102
    {
103
        $this->reflection = new \ReflectionMethod($classNameOrInstance, $methodName);
104
        $this->methodName = $methodName;
105
106
        if (!empty($cache)) {
107
            $this->constructFromCache($cache);
108
            $this->docBlockIsParsed = true;
109
        } else {
110
            $this->constructFromClassAndMethod($classNameOrInstance, $methodName);
111
        }
112
    }
113
114
    public static function create($classNameOrInstance, $methodName)
115
    {
116
        return new self($classNameOrInstance, $methodName);
117
    }
118
119
    public static function deserialize($cache)
120
    {
121
        $cache = (array)$cache;
122
123
        $classNameOrInstance = $cache['class'];
124
        $methodName = $cache['method_name'];
125
126
        // If the cache came from a newer version, ignore it and
127
        // regenerate the cached information.
128
        if (!static::isValidSerializedData($cache)) {
129
            return self::create($classNameOrInstance, $methodName);
130
        }
131
        return new self($classNameOrInstance, $methodName, $cache);
132
    }
133
134
    public static function isValidSerializedData($cache)
135
    {
136
        return
137
            isset($cache['schema']) &&
138
            ($cache['schema'] > 0) &&
139
            ($cache['schema'] <= self::SERIALIZATION_SCHEMA_VERSION);
140
    }
141
142
    protected function constructFromClassAndMethod($classNameOrInstance, $methodName)
0 ignored issues
show
Unused Code introduced by
The parameter $classNameOrInstance is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
143
    {
144
        $this->otherAnnotations = new AnnotationData();
145
        // Set up a default name for the command from the method name.
146
        // This can be overridden via @command or @name annotations.
147
        $this->name = $this->convertName($methodName);
148
        $this->options = new DefaultsWithDescriptions($this->determineOptionsFromParameters(), false);
149
        $this->arguments = $this->determineAgumentClassifications();
150
    }
151
152
    protected function constructFromCache($info_array)
153
    {
154
        $info_array += $this->defaultSerializationData();
155
156
        $this->name = $info_array['name'];
157
        $this->methodName = $info_array['method_name'];
158
        $this->otherAnnotations = new AnnotationData((array) $info_array['annotations']);
159
        $this->arguments = new DefaultsWithDescriptions();
160
        $this->options = new DefaultsWithDescriptions();
161
        $this->aliases = $info_array['aliases'];
162
        $this->help = $info_array['help'];
163
        $this->description = $info_array['description'];
164
        $this->exampleUsage = $info_array['example_usages'];
165
        $this->returnType = $info_array['return_type'];
0 ignored issues
show
Documentation Bug introduced by
It seems like $info_array['return_type'] of type array is incompatible with the declared type string of property $returnType.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
166
167 View Code Duplication
        foreach ((array)$info_array['arguments'] as $key => $info) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
168
            $info = (array)$info;
169
            $this->arguments->add($key, $info['description']);
170
            if (array_key_exists('default', $info)) {
171
                $this->arguments->setDefaultValue($key, $info['default']);
172
            }
173
        }
174 View Code Duplication
        foreach ((array)$info_array['options'] as $key => $info) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
175
            $info = (array)$info;
176
            $this->options->add($key, $info['description']);
177
            if (array_key_exists('default', $info)) {
178
                $this->options->setDefaultValue($key, $info['default']);
179
            }
180
        }
181
182
        $this->input_options = [];
0 ignored issues
show
Bug introduced by
The property input_options does not seem to exist. Did you mean options?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
183
        foreach ((array)$info_array['input_options'] as $i => $option) {
184
            $option = (array) $option;
185
            $this->inputOptions[$i] = new InputOption(
186
                $option['name'],
187
                $option['shortcut'],
188
                $option['mode'],
189
                $option['description'],
190
                $option['default']
191
            );
192
        }
193
    }
194
195
    public function serialize()
196
    {
197
        $info = [
198
            'schema' => self::SERIALIZATION_SCHEMA_VERSION,
199
            'class' => $this->reflection->getDeclaringClass()->getName(),
200
            'method_name' => $this->getMethodName(),
201
            'name' => $this->getName(),
202
            'description' => $this->getDescription(),
203
            'help' => $this->getHelp(),
204
            'aliases' => $this->getAliases(),
205
            'annotations' => $this->getAnnotations()->getArrayCopy(),
206
            // Todo: Test This.
207
            'topics' => $this->getTopics(),
208
            'example_usages' => $this->getExampleUsages(),
209
            'return_type' => $this->getReturnType(),
210
        ] + $this->defaultSerializationData();
211 View Code Duplication
        foreach ($this->arguments()->getValues() as $key => $val) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
212
            $info['arguments'][$key] = [
213
                'description' => $this->arguments()->getDescription($key),
214
            ];
215
            if ($this->arguments()->hasDefault($key)) {
216
                $info['arguments'][$key]['default'] = $val;
217
            }
218
        }
219 View Code Duplication
        foreach ($this->options()->getValues() as $key => $val) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
220
            $info['options'][$key] = [
221
                'description' => $this->options()->getDescription($key),
222
            ];
223
            if ($this->options()->hasDefault($key)) {
224
                $info['options'][$key]['default'] = $val;
225
            }
226
        }
227
        foreach ($this->getParameters() as $i => $parameter) {
0 ignored issues
show
Unused Code introduced by
This foreach statement is empty and can be removed.

This check looks for foreach loops that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

Consider removing the loop.

Loading history...
228
            // TODO: Also cache input/output params
229
        }
230
        foreach ($this->inputOptions() as $i => $option) {
231
            $mode = 0;
232
            if ($option->isValueRequired()) {
233
                $mode |= InputOption::VALUE_REQUIRED;
234
            }
235
            if ($option->isValueOptional()) {
236
                $mode |= InputOption::VALUE_OPTIONAL;
237
            }
238
            if ($option->isArray()) {
239
                $mode |= InputOption::VALUE_IS_ARRAY;
240
            }
241
            if (!$mode) {
242
                $mode = InputOption::VALUE_NONE;
243
            }
244
245
            $info['input_options'][$i] = [
246
                'name' => $option->getName(),
247
                'shortcut' => $option->getShortcut(),
248
                'mode' => $mode,
249
                'description' => $option->getDescription(),
250
                'default' => null,
251
            ];
252
            if ($option->isValueOptional()) {
253
                $info['input_options'][$i]['default'] = $option->getDefault();
254
            }
255
        }
256
        return $info;
257
    }
258
259
    /**
260
     * Default data for serialization.
261
     * @return array
262
     */
263
    protected function defaultSerializationData()
264
    {
265
        return [
266
            'description' => '',
267
            'help' => '',
268
            'aliases' => [],
269
            'annotations' => [],
270
            'topics' => [],
271
            'example_usages' => [],
272
            'return_type' => [],
273
            'parameters' => [],
274
            'arguments' => [],
275
            'arguments' => [],
276
            'options' => [],
277
            'input_options' => [],
278
        ];
279
    }
280
281
    /**
282
     * Recover the method name provided to the constructor.
283
     *
284
     * @return string
285
     */
286
    public function getMethodName()
287
    {
288
        return $this->methodName;
289
    }
290
291
    /**
292
     * Return the primary name for this command.
293
     *
294
     * @return string
295
     */
296
    public function getName()
297
    {
298
        $this->parseDocBlock();
299
        return $this->name;
300
    }
301
302
    /**
303
     * Set the primary name for this command.
304
     *
305
     * @param string $name
306
     */
307
    public function setName($name)
308
    {
309
        $this->name = $name;
310
        return $this;
311
    }
312
313
    public function getReturnType()
314
    {
315
        $this->parseDocBlock();
316
        return $this->returnType;
317
    }
318
319
    public function setReturnType($returnType)
320
    {
321
        $this->returnType = $returnType;
322
        return $this;
323
    }
324
325
    /**
326
     * Get any annotations included in the docblock comment for the
327
     * implementation method of this command that are not already
328
     * handled by the primary methods of this class.
329
     *
330
     * @return AnnotationData
331
     */
332
    public function getRawAnnotations()
333
    {
334
        $this->parseDocBlock();
335
        return $this->otherAnnotations;
336
    }
337
338
    /**
339
     * Get any annotations included in the docblock comment,
340
     * also including default values such as @command.  We add
341
     * in the default @command annotation late, and only in a
342
     * copy of the annotation data because we use the existance
343
     * of a @command to indicate that this CommandInfo is
344
     * a command, and not a hook or anything else.
345
     *
346
     * @return AnnotationData
347
     */
348
    public function getAnnotations()
349
    {
350
        return new AnnotationData(
351
            $this->getRawAnnotations()->getArrayCopy() +
352
            [
353
                'command' => $this->getName(),
354
            ]
355
        );
356
    }
357
358
    /**
359
     * Return a specific named annotation for this command.
360
     *
361
     * @param string $annotation The name of the annotation.
362
     * @return string
363
     */
364
    public function getAnnotation($annotation)
365
    {
366
        // hasAnnotation parses the docblock
367
        if (!$this->hasAnnotation($annotation)) {
368
            return null;
369
        }
370
        return $this->otherAnnotations[$annotation];
371
    }
372
373
    /**
374
     * Check to see if the specified annotation exists for this command.
375
     *
376
     * @param string $annotation The name of the annotation.
377
     * @return boolean
378
     */
379
    public function hasAnnotation($annotation)
380
    {
381
        $this->parseDocBlock();
382
        return isset($this->otherAnnotations[$annotation]);
383
    }
384
385
    /**
386
     * Save any tag that we do not explicitly recognize in the
387
     * 'otherAnnotations' map.
388
     */
389
    public function addAnnotation($name, $content)
390
    {
391
        $this->otherAnnotations[$name] = $content;
392
    }
393
394
    /**
395
     * Remove an annotation that was previoudly set.
396
     */
397
    public function removeAnnotation($name)
398
    {
399
        unset($this->otherAnnotations[$name]);
400
    }
401
402
    /**
403
     * Get the synopsis of the command (~first line).
404
     *
405
     * @return string
406
     */
407
    public function getDescription()
408
    {
409
        $this->parseDocBlock();
410
        return $this->description;
411
    }
412
413
    /**
414
     * Set the command description.
415
     *
416
     * @param string $description The description to set.
417
     */
418
    public function setDescription($description)
419
    {
420
        $this->description = $description;
421
        return $this;
422
    }
423
424
    /**
425
     * Get the help text of the command (the description)
426
     */
427
    public function getHelp()
428
    {
429
        $this->parseDocBlock();
430
        return $this->help;
431
    }
432
    /**
433
     * Set the help text for this command.
434
     *
435
     * @param string $help The help text.
436
     */
437
    public function setHelp($help)
438
    {
439
        $this->help = $help;
440
        return $this;
441
    }
442
443
    /**
444
     * Return the list of aliases for this command.
445
     * @return string[]
446
     */
447
    public function getAliases()
448
    {
449
        $this->parseDocBlock();
450
        return $this->aliases;
451
    }
452
453
    /**
454
     * Set aliases that can be used in place of the command's primary name.
455
     *
456
     * @param string|string[] $aliases
457
     */
458
    public function setAliases($aliases)
459
    {
460
        if (is_string($aliases)) {
461
            $aliases = explode(',', static::convertListToCommaSeparated($aliases));
462
        }
463
        $this->aliases = array_filter($aliases);
464
        return $this;
465
    }
466
467
    /**
468
     * Return the examples for this command. This is @usage instead of
469
     * @example because the later is defined by the phpdoc standard to
470
     * be example method calls.
471
     *
472
     * @return string[]
473
     */
474
    public function getExampleUsages()
475
    {
476
        $this->parseDocBlock();
477
        return $this->exampleUsage;
478
    }
479
480
    /**
481
     * Add an example usage for this command.
482
     *
483
     * @param string $usage An example of the command, including the command
484
     *   name and all of its example arguments and options.
485
     * @param string $description An explanation of what the example does.
486
     */
487
    public function setExampleUsage($usage, $description)
488
    {
489
        $this->exampleUsage[$usage] = $description;
490
        return $this;
491
    }
492
493
    /**
494
     * Return the topics for this command.
495
     *
496
     * @return string[]
497
     */
498
    public function getTopics()
499
    {
500
        if (!$this->hasAnnotation('topics')) {
501
            return [];
502
        }
503
        $topics = $this->getAnnotation('topics');
504
        return explode(',', trim($topics));
505
    }
506
507
    /**
508
     * Return the list of refleaction parameters.
509
     *
510
     * @return ReflectionParameter[]
511
     */
512
    public function getParameters()
513
    {
514
        return $this->reflection->getParameters();
515
    }
516
517
    /**
518
     * Descriptions of commandline arguements for this command.
519
     *
520
     * @return DefaultsWithDescriptions
521
     */
522
    public function arguments()
523
    {
524
        return $this->arguments;
525
    }
526
527
    /**
528
     * Descriptions of commandline options for this command.
529
     *
530
     * @return DefaultsWithDescriptions
531
     */
532
    public function options()
533
    {
534
        return $this->options;
535
    }
536
537
    /**
538
     * Get the inputOptions for the options associated with this CommandInfo
539
     * object, e.g. via @option annotations, or from
540
     * $options = ['someoption' => 'defaultvalue'] in the command method
541
     * parameter list.
542
     *
543
     * @return InputOption[]
544
     */
545
    public function inputOptions()
546
    {
547
        if (!isset($this->inputOptions)) {
548
            $this->inputOptions = $this->createInputOptions();
549
        }
550
        return $this->inputOptions;
551
    }
552
553
    protected function createInputOptions()
554
    {
555
        $explicitOptions = [];
556
557
        $opts = $this->options()->getValues();
558
        foreach ($opts as $name => $defaultValue) {
559
            $description = $this->options()->getDescription($name);
560
561
            $fullName = $name;
562
            $shortcut = '';
563
            if (strpos($name, '|')) {
564
                list($fullName, $shortcut) = explode('|', $name, 2);
565
            }
566
567
            if (is_bool($defaultValue)) {
568
                $explicitOptions[$fullName] = new InputOption($fullName, $shortcut, InputOption::VALUE_NONE, $description);
569
            } elseif ($defaultValue === InputOption::VALUE_REQUIRED) {
570
                $explicitOptions[$fullName] = new InputOption($fullName, $shortcut, InputOption::VALUE_REQUIRED, $description);
571
            } else {
572
                $explicitOptions[$fullName] = new InputOption($fullName, $shortcut, InputOption::VALUE_OPTIONAL, $description, $defaultValue);
573
            }
574
        }
575
576
        return $explicitOptions;
577
    }
578
579
    /**
580
     * An option might have a name such as 'silent|s'. In this
581
     * instance, we will allow the @option or @default tag to
582
     * reference the option only by name (e.g. 'silent' or 's'
583
     * instead of 'silent|s').
584
     *
585
     * @param string $optionName
586
     * @return string
587
     */
588
    public function findMatchingOption($optionName)
589
    {
590
        // Exit fast if there's an exact match
591
        if ($this->options->exists($optionName)) {
592
            return $optionName;
593
        }
594
        $existingOptionName = $this->findExistingOption($optionName);
595
        if (isset($existingOptionName)) {
596
            return $existingOptionName;
597
        }
598
        return $this->findOptionAmongAlternatives($optionName);
599
    }
600
601
    /**
602
     * @param string $optionName
603
     * @return string
604
     */
605
    protected function findOptionAmongAlternatives($optionName)
606
    {
607
        // Check the other direction: if the annotation contains @silent|s
608
        // and the options array has 'silent|s'.
609
        $checkMatching = explode('|', $optionName);
610
        if (count($checkMatching) > 1) {
611
            foreach ($checkMatching as $checkName) {
612
                if ($this->options->exists($checkName)) {
613
                    $this->options->rename($checkName, $optionName);
614
                    return $optionName;
615
                }
616
            }
617
        }
618
        return $optionName;
619
    }
620
621
    /**
622
     * @param string $optionName
623
     * @return string|null
624
     */
625
    protected function findExistingOption($optionName)
626
    {
627
        // Check to see if we can find the option name in an existing option,
628
        // e.g. if the options array has 'silent|s' => false, and the annotation
629
        // is @silent.
630
        foreach ($this->options()->getValues() as $name => $default) {
631
            if (in_array($optionName, explode('|', $name))) {
632
                return $name;
633
            }
634
        }
635
    }
636
637
    /**
638
     * Examine the parameters of the method for this command, and
639
     * build a list of commandline arguements for them.
640
     *
641
     * @return array
642
     */
643
    protected function determineAgumentClassifications()
644
    {
645
        $result = new DefaultsWithDescriptions();
646
        $params = $this->reflection->getParameters();
647
        $optionsFromParameters = $this->determineOptionsFromParameters();
648
        if (!empty($optionsFromParameters)) {
649
            array_pop($params);
650
        }
651
        foreach ($params as $param) {
652
            $this->addParameterToResult($result, $param);
653
        }
654
        return $result;
655
    }
656
657
    /**
658
     * Examine the provided parameter, and determine whether it
659
     * is a parameter that will be filled in with a positional
660
     * commandline argument.
661
     */
662
    protected function addParameterToResult($result, $param)
663
    {
664
        // Commandline arguments must be strings, so ignore any
665
        // parameter that is typehinted to any non-primative class.
666
        if ($param->getClass() != null) {
667
            return;
668
        }
669
        $result->add($param->name);
670
        if ($param->isDefaultValueAvailable()) {
671
            $defaultValue = $param->getDefaultValue();
672
            if (!$this->isAssoc($defaultValue)) {
673
                $result->setDefaultValue($param->name, $defaultValue);
674
            }
675
        } elseif ($param->isArray()) {
676
            $result->setDefaultValue($param->name, []);
677
        }
678
    }
679
680
    /**
681
     * Examine the parameters of the method for this command, and determine
682
     * the disposition of the options from them.
683
     *
684
     * @return array
685
     */
686
    protected function determineOptionsFromParameters()
687
    {
688
        $params = $this->reflection->getParameters();
689
        if (empty($params)) {
690
            return [];
691
        }
692
        $param = end($params);
693
        if (!$param->isDefaultValueAvailable()) {
694
            return [];
695
        }
696
        if (!$this->isAssoc($param->getDefaultValue())) {
697
            return [];
698
        }
699
        return $param->getDefaultValue();
700
    }
701
702
    /**
703
     * Helper; determine if an array is associative or not. An array
704
     * is not associative if its keys are numeric, and numbered sequentially
705
     * from zero. All other arrays are considered to be associative.
706
     *
707
     * @param arrau $arr The array
708
     * @return boolean
709
     */
710
    protected function isAssoc($arr)
711
    {
712
        if (!is_array($arr)) {
713
            return false;
714
        }
715
        return array_keys($arr) !== range(0, count($arr) - 1);
716
    }
717
718
    /**
719
     * Convert from a method name to the corresponding command name. A
720
     * method 'fooBar' will become 'foo:bar', and 'fooBarBazBoz' will
721
     * become 'foo:bar-baz-boz'.
722
     *
723
     * @param string $camel method name.
724
     * @return string
725
     */
726
    protected function convertName($camel)
727
    {
728
        $splitter="-";
729
        $camel=preg_replace('/(?!^)[[:upper:]][[:lower:]]/', '$0', preg_replace('/(?!^)[[:upper:]]+/', $splitter.'$0', $camel));
730
        $camel = preg_replace("/$splitter/", ':', $camel, 1);
731
        return strtolower($camel);
732
    }
733
734
    /**
735
     * Parse the docBlock comment for this command, and set the
736
     * fields of this class with the data thereby obtained.
737
     */
738
    protected function parseDocBlock()
739
    {
740
        if (!$this->docBlockIsParsed) {
741
            // The parse function will insert data from the provided method
742
            // into this object, using our accessors.
743
            CommandDocBlockParserFactory::parse($this, $this->reflection);
744
            $this->docBlockIsParsed = true;
745
        }
746
    }
747
748
    /**
749
     * Given a list that might be 'a b c' or 'a, b, c' or 'a,b,c',
750
     * convert the data into the last of these forms.
751
     */
752
    protected static function convertListToCommaSeparated($text)
753
    {
754
        return preg_replace('#[ \t\n\r,]+#', ',', $text);
755
    }
756
}
757