Completed
Pull Request — master (#175)
by Greg
02:29
created

StdinHandler::getStream()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 7
c 0
b 0
f 0
rs 10
cc 2
nc 2
nop 0
1
<?php
2
3
namespace Consolidation\AnnotatedCommand\Input;
4
5
use Symfony\Component\Console\Input\StreamableInputInterface;
6
use Symfony\Component\Console\Input\InputInterface;
7
8
/**
9
 * StdinHandler is a thin wrapper around php://stdin. It provides
10
 * methods for redirecting input from a file, possibly conditionally
11
 * under the control of an Input object.
12
 *
13
 * Example trivial usage (always reads from stdin):
14
 *
15
 *      class Example implements StdinAwareInterface
16
 *      {
17
 *          /**
18
 *           * @command cat
19
 *           * @param string $file
20
 *           * @default $file -
21
 *           * /
22
 *          public function cat()
23
 *          {
24
 *               print($this->stdin()->contents());
25
 *          }
26
 *      }
27
 *
28
 * Command that reads from stdin or file via an option:
29
 *
30
 *      /**
31
 *       * @command cat
32
 *       * @param string $file
33
 *       * @default $file -
34
 *       * /
35
 *      public function cat(InputInterface $input)
36
 *      {
37
 *          $data = $this->stdin()->select($input, 'file')->contents();
38
 *      }
39
 *
40
 * Command that reads from stdin or file via an option:
41
 *
42
 *      /**
43
 *       * @command cat
44
 *       * @option string $file
45
 *       * @default $file -
46
 *       * /
47
 *      public function cat(InputInterface $input)
48
 *      {
49
 *          $data = $this->stdin()->select($input, 'file')->contents();
50
 *      }
51
 *
52
 * It is also possible to inject the selected stream into the input object,
53
 * e.g. if you want the contents of the source file to be fed to any Question
54
 * helper et. al. that the $input object is used with.
55
 *
56
 *      /**
57
 *       * @command example
58
 *       * /
59
 *      public function example($file)
60
 *      {
61
 *          $this->stdin()->setStream($this->input, 'file');
62
 *      }
63
 *
64
 *
65
 * Inject an alternate source for standard input in tests.  Presumes that
66
 * the object under test gets a reference to the StdinHandler via dependency
67
 * injection from the container.
68
 *
69
 *      $container->get('stdinHandler')->redirect($pathToTestStdinFileFixture);
70
 *
71
 * You may also inject your stdin file fixture stream into the $input object
72
 * as usual, and then use it with 'select()' or 'setStream()' as shown above.
73
 */
74
class StdinHandler
75
{
76
    protected $path;
77
    protected $stream;
78
79
    /**
80
     * hasPath returns 'true' if the stdin handler has a path to a file.
81
     *
82
     * @return bool
83
     */
84
    public function hasPath()
85
    {
86
        // Once the stream has been opened, we mask the existence of the path.
87
        return !$this->hasStream() && !empty($this->path);
88
    }
89
90
    /**
91
     * hasStream returns 'true' if the stdin handler has opened a stream.
92
     *
93
     * @return bool
94
     */
95
    public function hasStream()
96
    {
97
        return !empty($this->stream);
98
    }
99
100
    /**
101
     * path returns the path to any file that was set as a redirection
102
     * source, or `php://stdin` if none have been.
103
     *
104
     * @return string
105
     */
106
    public function path()
107
    {
108
        return $this->path ?: 'php://stdin';
109
    }
110
111
    /**
112
     * close closes the input stream if it was opened.
113
     */
114
    public function close()
115
    {
116
        if ($this->hasStream()) {
117
            fclose($this->stream);
118
            $this->stream = null;
119
        }
120
        return $this;
121
    }
122
123
    /**
124
     * redirect specifies a path to a file that should serve as the
125
     * source to read from. If the input path is '-' or empty,
126
     * then output will be taken from php://stdin (or whichever source
127
     * was provided via the 'redirect' method).
128
     *
129
     * @return $this
130
     */
131
    public function redirect($path)
132
    {
133
        if ($this->pathProvided($path)) {
134
            $this->path = $path;
135
        }
136
137
        return $this;
138
    }
139
140
    /**
141
     * select chooses the source of the input stream based on whether or
142
     * not the user provided the specified option or argument on the commandline.
143
     * Stdin is selected if there is no user selection.
144
     *
145
     * @param InputInterface $input
146
     * @param string $optionOrArg
147
     * @return $this
148
     */
149
    public function select(InputInterface $input, $optionOrArg)
150
    {
151
        $this->redirect($this->getOptionOrArg($input, $optionOrArg));
152
        if (!$this->hasPath() && ($input instanceof StreamableInputInterface)) {
153
            $this->stream = $input->getStream();
154
        }
155
156
        return $this;
157
    }
158
159
    /**
160
     * getStream opens and returns the stdin stream (or redirect file).
161
     */
162
    public function getStream()
163
    {
164
        if (!$this->hasStream()) {
165
            $this->stream = fopen($this->path());
166
        }
167
        return $this->stream;
168
    }
169
170
    /**
171
     * setStream functions like 'select', and also sets up the $input
172
     * object to read from the selected input stream e.g. when used
173
     * with a question helper.
174
     */
175
    public function setStream(InputInterface $input, $optionOrArg)
176
    {
177
        $this->select($input, $optionOrArg);
178
        if ($input instanceof StreamableInputInterface) {
179
            $stream = $this->getStream();
180
            $input->setStream($stream);
0 ignored issues
show
Bug introduced by
It seems like $stream defined by $this->getStream() on line 179 can also be of type null; however, Symfony\Component\Consol...tInterface::setStream() does only seem to accept resource, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
181
        }
182
        return $this;
183
    }
184
185
    /**
186
     * contents reads the entire contents of the standard input stream.
187
     *
188
     * @return string
189
     */
190
    public function contents()
191
    {
192
        // Optimization: use file_get_contents if we have a path to a file
193
        // and the stream has not been opened yet.
194
        if (!$this->hasStream()) {
195
            return file_get_contents($this->path());
196
        }
197
        $stream = $this->getStream();
198
        stream_set_blocking($stream, false); // TODO: We did this in backend invoke. Necessary here?
199
        $contents = stream_get_contents($stream);
200
        $this->close();
201
202
        return $contents;
203
    }
204
205
    /**
206
     * Returns 'true' if a path was specfied, and that path was not '-'.
207
     */
208
    protected function pathProvided($path)
209
    {
210
        return !empty($path) && ($path != '-');
211
    }
212
213
    protected function getOptionOrArg(InputInterface $input, $optionOrArg)
214
    {
215
        if ($input->hasOption($optionOrArg)) {
216
            return $input->getOption($optionOrArg);
217
        }
218
        return $input->getArgument($optionOrArg);
219
    }
220
}
221