Removed modules/contrib/media module to allow update to the core media module
[yaffs-website] / vendor / psy / psysh / src / Configuration.php
1 <?php
2
3 /*
4  * This file is part of Psy Shell.
5  *
6  * (c) 2012-2018 Justin Hileman
7  *
8  * For the full copyright and license information, please view the LICENSE
9  * file that was distributed with this source code.
10  */
11
12 namespace Psy;
13
14 use Psy\Exception\DeprecatedException;
15 use Psy\Exception\RuntimeException;
16 use Psy\Output\OutputPager;
17 use Psy\Output\ShellOutput;
18 use Psy\Readline\GNUReadline;
19 use Psy\Readline\HoaConsole;
20 use Psy\Readline\Libedit;
21 use Psy\Readline\Readline;
22 use Psy\Readline\Transient;
23 use Psy\TabCompletion\AutoCompleter;
24 use Psy\VarDumper\Presenter;
25 use Psy\VersionUpdater\Checker;
26 use Psy\VersionUpdater\GitHubChecker;
27 use Psy\VersionUpdater\IntervalChecker;
28 use Psy\VersionUpdater\NoopChecker;
29
30 /**
31  * The Psy Shell configuration.
32  */
33 class Configuration
34 {
35     const COLOR_MODE_AUTO     = 'auto';
36     const COLOR_MODE_FORCED   = 'forced';
37     const COLOR_MODE_DISABLED = 'disabled';
38
39     private static $AVAILABLE_OPTIONS = [
40         'codeCleaner',
41         'colorMode',
42         'configDir',
43         'dataDir',
44         'defaultIncludes',
45         'eraseDuplicates',
46         'errorLoggingLevel',
47         'forceArrayIndexes',
48         'historySize',
49         'manualDbFile',
50         'pager',
51         'prompt',
52         'requireSemicolons',
53         'runtimeDir',
54         'startupMessage',
55         'updateCheck',
56         'useBracketedPaste',
57         'usePcntl',
58         'useReadline',
59         'useTabCompletion',
60         'useUnicode',
61         'warnOnMultipleConfigs',
62     ];
63
64     private $defaultIncludes;
65     private $configDir;
66     private $dataDir;
67     private $runtimeDir;
68     private $configFile;
69     /** @var string|false */
70     private $historyFile;
71     private $historySize;
72     private $eraseDuplicates;
73     private $manualDbFile;
74     private $hasReadline;
75     private $useReadline;
76     private $useBracketedPaste;
77     private $hasPcntl;
78     private $usePcntl;
79     private $newCommands       = [];
80     private $requireSemicolons = false;
81     private $useUnicode;
82     private $useTabCompletion;
83     private $newMatchers = [];
84     private $errorLoggingLevel = E_ALL;
85     private $warnOnMultipleConfigs = false;
86     private $colorMode;
87     private $updateCheck;
88     private $startupMessage;
89     private $forceArrayIndexes = false;
90
91     // services
92     private $readline;
93     private $output;
94     private $shell;
95     private $cleaner;
96     private $pager;
97     private $manualDb;
98     private $presenter;
99     private $autoCompleter;
100     private $checker;
101     private $prompt;
102
103     /**
104      * Construct a Configuration instance.
105      *
106      * Optionally, supply an array of configuration values to load.
107      *
108      * @param array $config Optional array of configuration values
109      */
110     public function __construct(array $config = [])
111     {
112         $this->setColorMode(self::COLOR_MODE_AUTO);
113
114         // explicit configFile option
115         if (isset($config['configFile'])) {
116             $this->configFile = $config['configFile'];
117         } elseif ($configFile = getenv('PSYSH_CONFIG')) {
118             $this->configFile = $configFile;
119         }
120
121         // legacy baseDir option
122         if (isset($config['baseDir'])) {
123             $msg = "The 'baseDir' configuration option is deprecated; " .
124                 "please specify 'configDir' and 'dataDir' options instead";
125             throw new DeprecatedException($msg);
126         }
127
128         unset($config['configFile'], $config['baseDir']);
129
130         // go go gadget, config!
131         $this->loadConfig($config);
132         $this->init();
133     }
134
135     /**
136      * Initialize the configuration.
137      *
138      * This checks for the presence of Readline and Pcntl extensions.
139      *
140      * If a config file is available, it will be loaded and merged with the current config.
141      *
142      * If no custom config file was specified and a local project config file
143      * is available, it will be loaded and merged with the current config.
144      */
145     public function init()
146     {
147         // feature detection
148         $this->hasReadline = function_exists('readline');
149         $this->hasPcntl    = function_exists('pcntl_signal') && function_exists('posix_getpid');
150
151         if ($configFile = $this->getConfigFile()) {
152             $this->loadConfigFile($configFile);
153         }
154
155         if (!$this->configFile && $localConfig = $this->getLocalConfigFile()) {
156             $this->loadConfigFile($localConfig);
157         }
158     }
159
160     /**
161      * Get the current PsySH config file.
162      *
163      * If a `configFile` option was passed to the Configuration constructor,
164      * this file will be returned. If not, all possible config directories will
165      * be searched, and the first `config.php` or `rc.php` file which exists
166      * will be returned.
167      *
168      * If you're trying to decide where to put your config file, pick
169      *
170      *     ~/.config/psysh/config.php
171      *
172      * @return string
173      */
174     public function getConfigFile()
175     {
176         if (isset($this->configFile)) {
177             return $this->configFile;
178         }
179
180         $files = ConfigPaths::getConfigFiles(['config.php', 'rc.php'], $this->configDir);
181
182         if (!empty($files)) {
183             if ($this->warnOnMultipleConfigs && count($files) > 1) {
184                 $msg = sprintf('Multiple configuration files found: %s. Using %s', implode($files, ', '), $files[0]);
185                 trigger_error($msg, E_USER_NOTICE);
186             }
187
188             return $files[0];
189         }
190     }
191
192     /**
193      * Get the local PsySH config file.
194      *
195      * Searches for a project specific config file `.psysh.php` in the current
196      * working directory.
197      *
198      * @return string
199      */
200     public function getLocalConfigFile()
201     {
202         $localConfig = getcwd() . '/.psysh.php';
203
204         if (@is_file($localConfig)) {
205             return $localConfig;
206         }
207     }
208
209     /**
210      * Load configuration values from an array of options.
211      *
212      * @param array $options
213      */
214     public function loadConfig(array $options)
215     {
216         foreach (self::$AVAILABLE_OPTIONS as $option) {
217             if (isset($options[$option])) {
218                 $method = 'set' . ucfirst($option);
219                 $this->$method($options[$option]);
220             }
221         }
222
223         // legacy `tabCompletion` option
224         if (isset($options['tabCompletion'])) {
225             $msg = '`tabCompletion` is deprecated; use `useTabCompletion` instead.';
226             @trigger_error($msg, E_USER_DEPRECATED);
227
228             $this->setUseTabCompletion($options['tabCompletion']);
229         }
230
231         foreach (['commands', 'matchers', 'casters'] as $option) {
232             if (isset($options[$option])) {
233                 $method = 'add' . ucfirst($option);
234                 $this->$method($options[$option]);
235             }
236         }
237
238         // legacy `tabCompletionMatchers` option
239         if (isset($options['tabCompletionMatchers'])) {
240             $msg = '`tabCompletionMatchers` is deprecated; use `matchers` instead.';
241             @trigger_error($msg, E_USER_DEPRECATED);
242
243             $this->addMatchers($options['tabCompletionMatchers']);
244         }
245     }
246
247     /**
248      * Load a configuration file (default: `$HOME/.config/psysh/config.php`).
249      *
250      * This configuration instance will be available to the config file as $config.
251      * The config file may directly manipulate the configuration, or may return
252      * an array of options which will be merged with the current configuration.
253      *
254      * @throws \InvalidArgumentException if the config file returns a non-array result
255      *
256      * @param string $file
257      */
258     public function loadConfigFile($file)
259     {
260         $__psysh_config_file__ = $file;
261         $load = function ($config) use ($__psysh_config_file__) {
262             $result = require $__psysh_config_file__;
263             if ($result !== 1) {
264                 return $result;
265             }
266         };
267         $result = $load($this);
268
269         if (!empty($result)) {
270             if (is_array($result)) {
271                 $this->loadConfig($result);
272             } else {
273                 throw new \InvalidArgumentException('Psy Shell configuration must return an array of options');
274             }
275         }
276     }
277
278     /**
279      * Set files to be included by default at the start of each shell session.
280      *
281      * @param array $includes
282      */
283     public function setDefaultIncludes(array $includes = [])
284     {
285         $this->defaultIncludes = $includes;
286     }
287
288     /**
289      * Get files to be included by default at the start of each shell session.
290      *
291      * @return array
292      */
293     public function getDefaultIncludes()
294     {
295         return $this->defaultIncludes ?: [];
296     }
297
298     /**
299      * Set the shell's config directory location.
300      *
301      * @param string $dir
302      */
303     public function setConfigDir($dir)
304     {
305         $this->configDir = (string) $dir;
306     }
307
308     /**
309      * Get the current configuration directory, if any is explicitly set.
310      *
311      * @return string
312      */
313     public function getConfigDir()
314     {
315         return $this->configDir;
316     }
317
318     /**
319      * Set the shell's data directory location.
320      *
321      * @param string $dir
322      */
323     public function setDataDir($dir)
324     {
325         $this->dataDir = (string) $dir;
326     }
327
328     /**
329      * Get the current data directory, if any is explicitly set.
330      *
331      * @return string
332      */
333     public function getDataDir()
334     {
335         return $this->dataDir;
336     }
337
338     /**
339      * Set the shell's temporary directory location.
340      *
341      * @param string $dir
342      */
343     public function setRuntimeDir($dir)
344     {
345         $this->runtimeDir = (string) $dir;
346     }
347
348     /**
349      * Get the shell's temporary directory location.
350      *
351      * Defaults to  `/psysh` inside the system's temp dir unless explicitly
352      * overridden.
353      *
354      * @return string
355      */
356     public function getRuntimeDir()
357     {
358         if (!isset($this->runtimeDir)) {
359             $this->runtimeDir = ConfigPaths::getRuntimeDir();
360         }
361
362         if (!is_dir($this->runtimeDir)) {
363             mkdir($this->runtimeDir, 0700, true);
364         }
365
366         return $this->runtimeDir;
367     }
368
369     /**
370      * Set the readline history file path.
371      *
372      * @param string $file
373      */
374     public function setHistoryFile($file)
375     {
376         $this->historyFile = ConfigPaths::touchFileWithMkdir($file);
377     }
378
379     /**
380      * Get the readline history file path.
381      *
382      * Defaults to `/history` inside the shell's base config dir unless
383      * explicitly overridden.
384      *
385      * @return string
386      */
387     public function getHistoryFile()
388     {
389         if (isset($this->historyFile)) {
390             return $this->historyFile;
391         }
392
393         $files = ConfigPaths::getConfigFiles(['psysh_history', 'history'], $this->configDir);
394
395         if (!empty($files)) {
396             if ($this->warnOnMultipleConfigs && count($files) > 1) {
397                 $msg = sprintf('Multiple history files found: %s. Using %s', implode($files, ', '), $files[0]);
398                 trigger_error($msg, E_USER_NOTICE);
399             }
400
401             $this->setHistoryFile($files[0]);
402         } else {
403             // fallback: create our own history file
404             $dir = $this->configDir ?: ConfigPaths::getCurrentConfigDir();
405             $this->setHistoryFile($dir . '/psysh_history');
406         }
407
408         return $this->historyFile;
409     }
410
411     /**
412      * Set the readline max history size.
413      *
414      * @param int $value
415      */
416     public function setHistorySize($value)
417     {
418         $this->historySize = (int) $value;
419     }
420
421     /**
422      * Get the readline max history size.
423      *
424      * @return int
425      */
426     public function getHistorySize()
427     {
428         return $this->historySize;
429     }
430
431     /**
432      * Sets whether readline erases old duplicate history entries.
433      *
434      * @param bool $value
435      */
436     public function setEraseDuplicates($value)
437     {
438         $this->eraseDuplicates = (bool) $value;
439     }
440
441     /**
442      * Get whether readline erases old duplicate history entries.
443      *
444      * @return bool
445      */
446     public function getEraseDuplicates()
447     {
448         return $this->eraseDuplicates;
449     }
450
451     /**
452      * Get a temporary file of type $type for process $pid.
453      *
454      * The file will be created inside the current temporary directory.
455      *
456      * @see self::getRuntimeDir
457      *
458      * @param string $type
459      * @param int    $pid
460      *
461      * @return string Temporary file name
462      */
463     public function getTempFile($type, $pid)
464     {
465         return tempnam($this->getRuntimeDir(), $type . '_' . $pid . '_');
466     }
467
468     /**
469      * Get a filename suitable for a FIFO pipe of $type for process $pid.
470      *
471      * The pipe will be created inside the current temporary directory.
472      *
473      * @param string $type
474      * @param int    $pid
475      *
476      * @return string Pipe name
477      */
478     public function getPipe($type, $pid)
479     {
480         return sprintf('%s/%s_%s', $this->getRuntimeDir(), $type, $pid);
481     }
482
483     /**
484      * Check whether this PHP instance has Readline available.
485      *
486      * @return bool True if Readline is available
487      */
488     public function hasReadline()
489     {
490         return $this->hasReadline;
491     }
492
493     /**
494      * Enable or disable Readline usage.
495      *
496      * @param bool $useReadline
497      */
498     public function setUseReadline($useReadline)
499     {
500         $this->useReadline = (bool) $useReadline;
501     }
502
503     /**
504      * Check whether to use Readline.
505      *
506      * If `setUseReadline` as been set to true, but Readline is not actually
507      * available, this will return false.
508      *
509      * @return bool True if the current Shell should use Readline
510      */
511     public function useReadline()
512     {
513         return isset($this->useReadline) ? ($this->hasReadline && $this->useReadline) : $this->hasReadline;
514     }
515
516     /**
517      * Set the Psy Shell readline service.
518      *
519      * @param Readline $readline
520      */
521     public function setReadline(Readline $readline)
522     {
523         $this->readline = $readline;
524     }
525
526     /**
527      * Get the Psy Shell readline service.
528      *
529      * By default, this service uses (in order of preference):
530      *
531      *  * GNU Readline
532      *  * Libedit
533      *  * A transient array-based readline emulation.
534      *
535      * @return Readline
536      */
537     public function getReadline()
538     {
539         if (!isset($this->readline)) {
540             $className = $this->getReadlineClass();
541             $this->readline = new $className(
542                 $this->getHistoryFile(),
543                 $this->getHistorySize(),
544                 $this->getEraseDuplicates()
545             );
546         }
547
548         return $this->readline;
549     }
550
551     /**
552      * Get the appropriate Readline implementation class name.
553      *
554      * @see self::getReadline
555      *
556      * @return string
557      */
558     private function getReadlineClass()
559     {
560         if ($this->useReadline()) {
561             if (GNUReadline::isSupported()) {
562                 return 'Psy\Readline\GNUReadline';
563             } elseif (Libedit::isSupported()) {
564                 return 'Psy\Readline\Libedit';
565             } elseif (HoaConsole::isSupported()) {
566                 return 'Psy\Readline\HoaConsole';
567             }
568         }
569
570         return 'Psy\Readline\Transient';
571     }
572
573     /**
574      * Enable or disable bracketed paste.
575      *
576      * Note that this only works with readline (not libedit) integration for now.
577      *
578      * @param bool $useBracketedPaste
579      */
580     public function setUseBracketedPaste($useBracketedPaste)
581     {
582         $this->useBracketedPaste = (bool) $useBracketedPaste;
583     }
584
585     /**
586      * Check whether to use bracketed paste with readline.
587      *
588      * When this works, it's magical. Tabs in pastes don't try to autcomplete.
589      * Newlines in paste don't execute code until you get to the end. It makes
590      * readline act like you'd expect when pasting.
591      *
592      * But it often (usually?) does not work. And when it doesn't, it just spews
593      * escape codes all over the place and generally makes things ugly :(
594      *
595      * If `useBracketedPaste` has been set to true, but the current readline
596      * implementation is anything besides GNU readline, this will return false.
597      *
598      * @return bool True if the shell should use bracketed paste
599      */
600     public function useBracketedPaste()
601     {
602         // For now, only the GNU readline implementation supports bracketed paste.
603         $supported = ($this->getReadlineClass() === 'Psy\Readline\GNUReadline');
604
605         return $supported && $this->useBracketedPaste;
606
607         // @todo mebbe turn this on by default some day?
608         // return isset($this->useBracketedPaste) ? ($supported && $this->useBracketedPaste) : $supported;
609     }
610
611     /**
612      * Check whether this PHP instance has Pcntl available.
613      *
614      * @return bool True if Pcntl is available
615      */
616     public function hasPcntl()
617     {
618         return $this->hasPcntl;
619     }
620
621     /**
622      * Enable or disable Pcntl usage.
623      *
624      * @param bool $usePcntl
625      */
626     public function setUsePcntl($usePcntl)
627     {
628         $this->usePcntl = (bool) $usePcntl;
629     }
630
631     /**
632      * Check whether to use Pcntl.
633      *
634      * If `setUsePcntl` has been set to true, but Pcntl is not actually
635      * available, this will return false.
636      *
637      * @return bool True if the current Shell should use Pcntl
638      */
639     public function usePcntl()
640     {
641         return isset($this->usePcntl) ? ($this->hasPcntl && $this->usePcntl) : $this->hasPcntl;
642     }
643
644     /**
645      * Enable or disable strict requirement of semicolons.
646      *
647      * @see self::requireSemicolons()
648      *
649      * @param bool $requireSemicolons
650      */
651     public function setRequireSemicolons($requireSemicolons)
652     {
653         $this->requireSemicolons = (bool) $requireSemicolons;
654     }
655
656     /**
657      * Check whether to require semicolons on all statements.
658      *
659      * By default, PsySH will automatically insert semicolons at the end of
660      * statements if they're missing. To strictly require semicolons, set
661      * `requireSemicolons` to true.
662      *
663      * @return bool
664      */
665     public function requireSemicolons()
666     {
667         return $this->requireSemicolons;
668     }
669
670     /**
671      * Enable or disable Unicode in PsySH specific output.
672      *
673      * Note that this does not disable Unicode output in general, it just makes
674      * it so PsySH won't output any itself.
675      *
676      * @param bool $useUnicode
677      */
678     public function setUseUnicode($useUnicode)
679     {
680         $this->useUnicode = (bool) $useUnicode;
681     }
682
683     /**
684      * Check whether to use Unicode in PsySH specific output.
685      *
686      * Note that this does not disable Unicode output in general, it just makes
687      * it so PsySH won't output any itself.
688      *
689      * @return bool
690      */
691     public function useUnicode()
692     {
693         if (isset($this->useUnicode)) {
694             return $this->useUnicode;
695         }
696
697         // @todo detect `chsh` != 65001 on Windows and return false
698         return true;
699     }
700
701     /**
702      * Set the error logging level.
703      *
704      * @see self::errorLoggingLevel
705      *
706      * @param bool $errorLoggingLevel
707      */
708     public function setErrorLoggingLevel($errorLoggingLevel)
709     {
710         $this->errorLoggingLevel = (E_ALL | E_STRICT) & $errorLoggingLevel;
711     }
712
713     /**
714      * Get the current error logging level.
715      *
716      * By default, PsySH will automatically log all errors, regardless of the
717      * current `error_reporting` level. Additionally, if the `error_reporting`
718      * level warrants, an ErrorException will be thrown.
719      *
720      * Set `errorLoggingLevel` to 0 to prevent logging non-thrown errors. Set it
721      * to any valid error_reporting value to log only errors which match that
722      * level.
723      *
724      *     http://php.net/manual/en/function.error-reporting.php
725      *
726      * @return int
727      */
728     public function errorLoggingLevel()
729     {
730         return $this->errorLoggingLevel;
731     }
732
733     /**
734      * Set a CodeCleaner service instance.
735      *
736      * @param CodeCleaner $cleaner
737      */
738     public function setCodeCleaner(CodeCleaner $cleaner)
739     {
740         $this->cleaner = $cleaner;
741     }
742
743     /**
744      * Get a CodeCleaner service instance.
745      *
746      * If none has been explicitly defined, this will create a new instance.
747      *
748      * @return CodeCleaner
749      */
750     public function getCodeCleaner()
751     {
752         if (!isset($this->cleaner)) {
753             $this->cleaner = new CodeCleaner();
754         }
755
756         return $this->cleaner;
757     }
758
759     /**
760      * Enable or disable tab completion.
761      *
762      * @param bool $useTabCompletion
763      */
764     public function setUseTabCompletion($useTabCompletion)
765     {
766         $this->useTabCompletion = (bool) $useTabCompletion;
767     }
768
769     /**
770      * @deprecated Call `setUseTabCompletion` instead
771      *
772      * @param bool $useTabCompletion
773      */
774     public function setTabCompletion($useTabCompletion)
775     {
776         $this->setUseTabCompletion($useTabCompletion);
777     }
778
779     /**
780      * Check whether to use tab completion.
781      *
782      * If `setUseTabCompletion` has been set to true, but readline is not
783      * actually available, this will return false.
784      *
785      * @return bool True if the current Shell should use tab completion
786      */
787     public function useTabCompletion()
788     {
789         return isset($this->useTabCompletion) ? ($this->hasReadline && $this->useTabCompletion) : $this->hasReadline;
790     }
791
792     /**
793      * @deprecated Call `useTabCompletion` instead
794      *
795      * @return bool
796      */
797     public function getTabCompletion()
798     {
799         return $this->useTabCompletion();
800     }
801
802     /**
803      * Set the Shell Output service.
804      *
805      * @param ShellOutput $output
806      */
807     public function setOutput(ShellOutput $output)
808     {
809         $this->output = $output;
810     }
811
812     /**
813      * Get a Shell Output service instance.
814      *
815      * If none has been explicitly provided, this will create a new instance
816      * with VERBOSITY_NORMAL and the output page supplied by self::getPager
817      *
818      * @see self::getPager
819      *
820      * @return ShellOutput
821      */
822     public function getOutput()
823     {
824         if (!isset($this->output)) {
825             $this->output = new ShellOutput(
826                 ShellOutput::VERBOSITY_NORMAL,
827                 $this->getOutputDecorated(),
828                 null,
829                 $this->getPager()
830             );
831         }
832
833         return $this->output;
834     }
835
836     /**
837      * Get the decoration (i.e. color) setting for the Shell Output service.
838      *
839      * @return null|bool 3-state boolean corresponding to the current color mode
840      */
841     public function getOutputDecorated()
842     {
843         if ($this->colorMode() === self::COLOR_MODE_AUTO) {
844             return;
845         } elseif ($this->colorMode() === self::COLOR_MODE_FORCED) {
846             return true;
847         } elseif ($this->colorMode() === self::COLOR_MODE_DISABLED) {
848             return false;
849         }
850     }
851
852     /**
853      * Set the OutputPager service.
854      *
855      * If a string is supplied, a ProcOutputPager will be used which shells out
856      * to the specified command.
857      *
858      * @throws \InvalidArgumentException if $pager is not a string or OutputPager instance
859      *
860      * @param string|OutputPager $pager
861      */
862     public function setPager($pager)
863     {
864         if ($pager && !is_string($pager) && !$pager instanceof OutputPager) {
865             throw new \InvalidArgumentException('Unexpected pager instance');
866         }
867
868         $this->pager = $pager;
869     }
870
871     /**
872      * Get an OutputPager instance or a command for an external Proc pager.
873      *
874      * If no Pager has been explicitly provided, and Pcntl is available, this
875      * will default to `cli.pager` ini value, falling back to `which less`.
876      *
877      * @return string|OutputPager
878      */
879     public function getPager()
880     {
881         if (!isset($this->pager) && $this->usePcntl()) {
882             if ($pager = ini_get('cli.pager')) {
883                 // use the default pager (5.4+)
884                 $this->pager = $pager;
885             } elseif ($less = exec('which less 2>/dev/null')) {
886                 // check for the presence of less...
887                 $this->pager = $less . ' -R -S -F -X';
888             }
889         }
890
891         return $this->pager;
892     }
893
894     /**
895      * Set the Shell AutoCompleter service.
896      *
897      * @param AutoCompleter $autoCompleter
898      */
899     public function setAutoCompleter(AutoCompleter $autoCompleter)
900     {
901         $this->autoCompleter = $autoCompleter;
902     }
903
904     /**
905      * Get an AutoCompleter service instance.
906      *
907      * @return AutoCompleter
908      */
909     public function getAutoCompleter()
910     {
911         if (!isset($this->autoCompleter)) {
912             $this->autoCompleter = new AutoCompleter();
913         }
914
915         return $this->autoCompleter;
916     }
917
918     /**
919      * @deprecated Nothing should be using this anymore
920      *
921      * @return array
922      */
923     public function getTabCompletionMatchers()
924     {
925         return [];
926     }
927
928     /**
929      * Add tab completion matchers to the AutoCompleter.
930      *
931      * This will buffer new matchers in the event that the Shell has not yet
932      * been instantiated. This allows the user to specify matchers in their
933      * config rc file, despite the fact that their file is needed in the Shell
934      * constructor.
935      *
936      * @param array $matchers
937      */
938     public function addMatchers(array $matchers)
939     {
940         $this->newMatchers = array_merge($this->newMatchers, $matchers);
941         if (isset($this->shell)) {
942             $this->doAddMatchers();
943         }
944     }
945
946     /**
947      * Internal method for adding tab completion matchers. This will set any new
948      * matchers once a Shell is available.
949      */
950     private function doAddMatchers()
951     {
952         if (!empty($this->newMatchers)) {
953             $this->shell->addMatchers($this->newMatchers);
954             $this->newMatchers = [];
955         }
956     }
957
958     /**
959      * @deprecated Use `addMatchers` instead
960      *
961      * @param array $matchers
962      */
963     public function addTabCompletionMatchers(array $matchers)
964     {
965         $this->addMatchers($matchers);
966     }
967
968     /**
969      * Add commands to the Shell.
970      *
971      * This will buffer new commands in the event that the Shell has not yet
972      * been instantiated. This allows the user to specify commands in their
973      * config rc file, despite the fact that their file is needed in the Shell
974      * constructor.
975      *
976      * @param array $commands
977      */
978     public function addCommands(array $commands)
979     {
980         $this->newCommands = array_merge($this->newCommands, $commands);
981         if (isset($this->shell)) {
982             $this->doAddCommands();
983         }
984     }
985
986     /**
987      * Internal method for adding commands. This will set any new commands once
988      * a Shell is available.
989      */
990     private function doAddCommands()
991     {
992         if (!empty($this->newCommands)) {
993             $this->shell->addCommands($this->newCommands);
994             $this->newCommands = [];
995         }
996     }
997
998     /**
999      * Set the Shell backreference and add any new commands to the Shell.
1000      *
1001      * @param Shell $shell
1002      */
1003     public function setShell(Shell $shell)
1004     {
1005         $this->shell = $shell;
1006         $this->doAddCommands();
1007         $this->doAddMatchers();
1008     }
1009
1010     /**
1011      * Set the PHP manual database file.
1012      *
1013      * This file should be an SQLite database generated from the phpdoc source
1014      * with the `bin/build_manual` script.
1015      *
1016      * @param string $filename
1017      */
1018     public function setManualDbFile($filename)
1019     {
1020         $this->manualDbFile = (string) $filename;
1021     }
1022
1023     /**
1024      * Get the current PHP manual database file.
1025      *
1026      * @return string Default: '~/.local/share/psysh/php_manual.sqlite'
1027      */
1028     public function getManualDbFile()
1029     {
1030         if (isset($this->manualDbFile)) {
1031             return $this->manualDbFile;
1032         }
1033
1034         $files = ConfigPaths::getDataFiles(['php_manual.sqlite'], $this->dataDir);
1035         if (!empty($files)) {
1036             if ($this->warnOnMultipleConfigs && count($files) > 1) {
1037                 $msg = sprintf('Multiple manual database files found: %s. Using %s', implode($files, ', '), $files[0]);
1038                 trigger_error($msg, E_USER_NOTICE);
1039             }
1040
1041             return $this->manualDbFile = $files[0];
1042         }
1043     }
1044
1045     /**
1046      * Get a PHP manual database connection.
1047      *
1048      * @return \PDO
1049      */
1050     public function getManualDb()
1051     {
1052         if (!isset($this->manualDb)) {
1053             $dbFile = $this->getManualDbFile();
1054             if (is_file($dbFile)) {
1055                 try {
1056                     $this->manualDb = new \PDO('sqlite:' . $dbFile);
1057                 } catch (\PDOException $e) {
1058                     if ($e->getMessage() === 'could not find driver') {
1059                         throw new RuntimeException('SQLite PDO driver not found', 0, $e);
1060                     } else {
1061                         throw $e;
1062                     }
1063                 }
1064             }
1065         }
1066
1067         return $this->manualDb;
1068     }
1069
1070     /**
1071      * Add an array of casters definitions.
1072      *
1073      * @param array $casters
1074      */
1075     public function addCasters(array $casters)
1076     {
1077         $this->getPresenter()->addCasters($casters);
1078     }
1079
1080     /**
1081      * Get the Presenter service.
1082      *
1083      * @return Presenter
1084      */
1085     public function getPresenter()
1086     {
1087         if (!isset($this->presenter)) {
1088             $this->presenter = new Presenter($this->getOutput()->getFormatter(), $this->forceArrayIndexes());
1089         }
1090
1091         return $this->presenter;
1092     }
1093
1094     /**
1095      * Enable or disable warnings on multiple configuration or data files.
1096      *
1097      * @see self::warnOnMultipleConfigs()
1098      *
1099      * @param bool $warnOnMultipleConfigs
1100      */
1101     public function setWarnOnMultipleConfigs($warnOnMultipleConfigs)
1102     {
1103         $this->warnOnMultipleConfigs = (bool) $warnOnMultipleConfigs;
1104     }
1105
1106     /**
1107      * Check whether to warn on multiple configuration or data files.
1108      *
1109      * By default, PsySH will use the file with highest precedence, and will
1110      * silently ignore all others. With this enabled, a warning will be emitted
1111      * (but not an exception thrown) if multiple configuration or data files
1112      * are found.
1113      *
1114      * This will default to true in a future release, but is false for now.
1115      *
1116      * @return bool
1117      */
1118     public function warnOnMultipleConfigs()
1119     {
1120         return $this->warnOnMultipleConfigs;
1121     }
1122
1123     /**
1124      * Set the current color mode.
1125      *
1126      * @param string $colorMode
1127      */
1128     public function setColorMode($colorMode)
1129     {
1130         $validColorModes = [
1131             self::COLOR_MODE_AUTO,
1132             self::COLOR_MODE_FORCED,
1133             self::COLOR_MODE_DISABLED,
1134         ];
1135
1136         if (in_array($colorMode, $validColorModes)) {
1137             $this->colorMode = $colorMode;
1138         } else {
1139             throw new \InvalidArgumentException('invalid color mode: ' . $colorMode);
1140         }
1141     }
1142
1143     /**
1144      * Get the current color mode.
1145      *
1146      * @return string
1147      */
1148     public function colorMode()
1149     {
1150         return $this->colorMode;
1151     }
1152
1153     /**
1154      * Set an update checker service instance.
1155      *
1156      * @param Checker $checker
1157      */
1158     public function setChecker(Checker $checker)
1159     {
1160         $this->checker = $checker;
1161     }
1162
1163     /**
1164      * Get an update checker service instance.
1165      *
1166      * If none has been explicitly defined, this will create a new instance.
1167      *
1168      * @return Checker
1169      */
1170     public function getChecker()
1171     {
1172         if (!isset($this->checker)) {
1173             $interval = $this->getUpdateCheck();
1174             switch ($interval) {
1175                 case Checker::ALWAYS:
1176                     $this->checker = new GitHubChecker();
1177                     break;
1178
1179                 case Checker::DAILY:
1180                 case Checker::WEEKLY:
1181                 case Checker::MONTHLY:
1182                     $checkFile = $this->getUpdateCheckCacheFile();
1183                     if ($checkFile === false) {
1184                         $this->checker = new NoopChecker();
1185                     } else {
1186                         $this->checker = new IntervalChecker($checkFile, $interval);
1187                     }
1188                     break;
1189
1190                 case Checker::NEVER:
1191                     $this->checker = new NoopChecker();
1192                     break;
1193             }
1194         }
1195
1196         return $this->checker;
1197     }
1198
1199     /**
1200      * Get the current update check interval.
1201      *
1202      * One of 'always', 'daily', 'weekly', 'monthly' or 'never'. If none is
1203      * explicitly set, default to 'weekly'.
1204      *
1205      * @return string
1206      */
1207     public function getUpdateCheck()
1208     {
1209         return isset($this->updateCheck) ? $this->updateCheck : Checker::WEEKLY;
1210     }
1211
1212     /**
1213      * Set the update check interval.
1214      *
1215      * @throws \InvalidArgumentDescription if the update check interval is unknown
1216      *
1217      * @param string $interval
1218      */
1219     public function setUpdateCheck($interval)
1220     {
1221         $validIntervals = [
1222             Checker::ALWAYS,
1223             Checker::DAILY,
1224             Checker::WEEKLY,
1225             Checker::MONTHLY,
1226             Checker::NEVER,
1227         ];
1228
1229         if (!in_array($interval, $validIntervals)) {
1230             throw new \InvalidArgumentException('invalid update check interval: ' . $interval);
1231         }
1232
1233         $this->updateCheck = $interval;
1234     }
1235
1236     /**
1237      * Get a cache file path for the update checker.
1238      *
1239      * @return string|false Return false if config file/directory is not writable
1240      */
1241     public function getUpdateCheckCacheFile()
1242     {
1243         $dir = $this->configDir ?: ConfigPaths::getCurrentConfigDir();
1244
1245         return ConfigPaths::touchFileWithMkdir($dir . '/update_check.json');
1246     }
1247
1248     /**
1249      * Set the startup message.
1250      *
1251      * @param string $message
1252      */
1253     public function setStartupMessage($message)
1254     {
1255         $this->startupMessage = $message;
1256     }
1257
1258     /**
1259      * Get the startup message.
1260      *
1261      * @return string|null
1262      */
1263     public function getStartupMessage()
1264     {
1265         return $this->startupMessage;
1266     }
1267
1268     /**
1269      * Set the prompt.
1270      *
1271      * @param string $prompt
1272      */
1273     public function setPrompt($prompt)
1274     {
1275         $this->prompt = $prompt;
1276     }
1277
1278     /**
1279      * Get the prompt.
1280      *
1281      * @return string
1282      */
1283     public function getPrompt()
1284     {
1285         return $this->prompt;
1286     }
1287
1288     /**
1289      * Get the force array indexes.
1290      *
1291      * @return bool
1292      */
1293     public function forceArrayIndexes()
1294     {
1295         return $this->forceArrayIndexes;
1296     }
1297
1298     /**
1299      * Set the force array indexes.
1300      *
1301      * @param bool $forceArrayIndexes
1302      */
1303     public function setForceArrayIndexes($forceArrayIndexes)
1304     {
1305         $this->forceArrayIndexes = $forceArrayIndexes;
1306     }
1307 }