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