Completed
Pull Request — master (#175)
by Greg
01:45
created

StdinHandler::hasStream()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
c 0
b 0
f 0
rs 10
cc 1
nc 1
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
 * Finally, this class may also be used in absence of a dependency injection
75
 * container by using the static 'selectStream()' method:
76
 *
77
 *      /**
78
 *       * @command example
79
 *       * /
80
 *      public function example($file)
81
 *      {
82
 *          StdinHandler::selectStream($this->input, 'file');
83
 *      }
84
 *
85
 * To test a method that uses this technique, simply inject your stdin
86
 * fixture into the $input object in your test:
87
 *
88
 *      $input->setStream(fopen($pathToFixture, 'r'));
89
 */
90
class StdinHandler
91
{
92
    protected $path;
93
    protected $stream;
94
95
    public static function selectStream(InputInterface $input, $optionOrArg)
96
    {
97
        $handler = new Self();
98
99
        return $handler->setStream($input, $optionOrArg);
100
    }
101
102
    /**
103
     * hasPath returns 'true' if the stdin handler has a path to a file.
104
     *
105
     * @return bool
106
     */
107
    public function hasPath()
108
    {
109
        // Once the stream has been opened, we mask the existence of the path.
110
        return !$this->hasStream() && !empty($this->path);
111
    }
112
113
    /**
114
     * hasStream returns 'true' if the stdin handler has opened a stream.
115
     *
116
     * @return bool
117
     */
118
    public function hasStream()
119
    {
120
        return !empty($this->stream);
121
    }
122
123
    /**
124
     * path returns the path to any file that was set as a redirection
125
     * source, or `php://stdin` if none have been.
126
     *
127
     * @return string
128
     */
129
    public function path()
130
    {
131
        return $this->path ?: 'php://stdin';
132
    }
133
134
    /**
135
     * close closes the input stream if it was opened.
136
     */
137
    public function close()
138
    {
139
        if ($this->hasStream()) {
140
            fclose($this->stream);
141
            $this->stream = null;
142
        }
143
        return $this;
144
    }
145
146
    /**
147
     * redirect specifies a path to a file that should serve as the
148
     * source to read from. If the input path is '-' or empty,
149
     * then output will be taken from php://stdin (or whichever source
150
     * was provided via the 'redirect' method).
151
     *
152
     * @return $this
153
     */
154
    public function redirect($path)
155
    {
156
        if ($this->pathProvided($path)) {
157
            $this->path = $path;
158
        }
159
160
        return $this;
161
    }
162
163
    /**
164
     * select chooses the source of the input stream based on whether or
165
     * not the user provided the specified option or argument on the commandline.
166
     * Stdin is selected if there is no user selection.
167
     *
168
     * @param InputInterface $input
169
     * @param string $optionOrArg
170
     * @return $this
171
     */
172
    public function select(InputInterface $input, $optionOrArg)
173
    {
174
        $this->redirect($this->getOptionOrArg($input, $optionOrArg));
175
        if (!$this->hasPath() && ($input instanceof StreamableInputInterface)) {
176
            $this->stream = $input->getStream();
177
        }
178
179
        return $this;
180
    }
181
182
    /**
183
     * getStream opens and returns the stdin stream (or redirect file).
184
     */
185
    public function getStream()
186
    {
187
        if (!$this->hasStream()) {
188
            $this->stream = fopen($this->path(), 'r');
189
        }
190
        return $this->stream;
191
    }
192
193
    /**
194
     * setStream functions like 'select', and also sets up the $input
195
     * object to read from the selected input stream e.g. when used
196
     * with a question helper.
197
     */
198
    public function setStream(InputInterface $input, $optionOrArg)
199
    {
200
        $this->select($input, $optionOrArg);
201
        if ($input instanceof StreamableInputInterface) {
202
            $stream = $this->getStream();
203
            $input->setStream($stream);
0 ignored issues
show
Bug introduced by
It seems like $stream defined by $this->getStream() on line 202 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...
204
        }
205
        return $this;
206
    }
207
208
    /**
209
     * contents reads the entire contents of the standard input stream.
210
     *
211
     * @return string
212
     */
213
    public function contents()
214
    {
215
        // Optimization: use file_get_contents if we have a path to a file
216
        // and the stream has not been opened yet.
217
        if (!$this->hasStream()) {
218
            return file_get_contents($this->path());
219
        }
220
        $stream = $this->getStream();
221
        stream_set_blocking($stream, false); // TODO: We did this in backend invoke. Necessary here?
222
        $contents = stream_get_contents($stream);
223
        $this->close();
224
225
        return $contents;
226
    }
227
228
    /**
229
     * Returns 'true' if a path was specfied, and that path was not '-'.
230
     */
231
    protected function pathProvided($path)
232
    {
233
        return !empty($path) && ($path != '-');
234
    }
235
236
    protected function getOptionOrArg(InputInterface $input, $optionOrArg)
237
    {
238
        if ($input->hasOption($optionOrArg)) {
239
            return $input->getOption($optionOrArg);
240
        }
241
        return $input->getArgument($optionOrArg);
242
    }
243
}
244