1: <?php declare(strict_types=1);
2:
3: namespace Salient\Sync\Command;
4:
5: use Salient\Cli\Exception\CliInvalidArgumentsException;
6: use Salient\Cli\CliOption;
7: use Salient\Contract\Cli\CliOptionType;
8: use Salient\Contract\Cli\CliOptionValueType;
9: use Salient\Contract\Http\HttpRequestMethod as Method;
10: use Salient\Core\Facade\Console;
11: use Salient\Sync\Http\HttpSyncProvider;
12: use Salient\Utility\Arr;
13: use Salient\Utility\Get;
14: use Salient\Utility\Inflect;
15: use Salient\Utility\Json;
16: use Salient\Utility\Str;
17: use InvalidArgumentException;
18: use LogicException;
19:
20: /**
21: * Sends HTTP requests to HTTP sync providers
22: */
23: final class SendHttpSyncProviderRequest extends AbstractSyncCommand
24: {
25: private string $ProviderBasename = '';
26: /** @var class-string<HttpSyncProvider> */
27: private string $Provider = HttpSyncProvider::class;
28: private string $Endpoint = '';
29: /** @var string[] */
30: private array $Query = [];
31: private ?string $Data = null;
32: private bool $Paginate = false;
33: private bool $Stream = false;
34:
35: // --
36:
37: /** @var Method::HEAD|Method::GET|Method::POST|Method::PUT|Method::DELETE|Method::PATCH */
38: private string $Method;
39:
40: public function getDescription(): string
41: {
42: return sprintf(
43: 'Send a %s request to an HTTP provider',
44: $this->getMethod(),
45: );
46: }
47:
48: protected function getOptionList(): iterable
49: {
50: $method = $this->getMethod();
51: $builder = CliOption::build()
52: ->name('provider')
53: ->required();
54:
55: if ($this->HttpProviders) {
56: yield $builder
57: ->optionType(CliOptionType::ONE_OF_POSITIONAL)
58: ->allowedValues(array_keys($this->HttpProviders))
59: ->bindTo($this->ProviderBasename);
60: } else {
61: yield $builder
62: ->description('The fully-qualified name of the HTTP provider to use')
63: ->optionType(CliOptionType::VALUE_POSITIONAL)
64: ->bindTo($this->Provider);
65: }
66:
67: yield from [
68: CliOption::build()
69: ->name('endpoint')
70: ->description("The endpoint to request, e.g. '/posts'")
71: ->optionType(CliOptionType::VALUE_POSITIONAL)
72: ->required()
73: ->bindTo($this->Endpoint),
74: CliOption::build()
75: ->long('query')
76: ->short('q')
77: ->valueName('field=value')
78: ->description('A query parameter to apply to the request')
79: ->optionType(CliOptionType::VALUE)
80: ->multipleAllowed()
81: ->bindTo($this->Query),
82: ];
83:
84: if (!($method === Method::HEAD || $method === Method::GET)) {
85: yield CliOption::build()
86: ->long('data')
87: ->short('J')
88: ->valueName('file')
89: ->description('The path to JSON-serialized data to submit with the request')
90: ->optionType(CliOptionType::VALUE)
91: ->valueType(CliOptionValueType::FILE_OR_DASH)
92: ->bindTo($this->Data);
93: }
94:
95: if ($method === Method::GET || $method === Method::POST) {
96: yield from [
97: CliOption::build()
98: ->long('paginate')
99: ->short('P')
100: ->description('Use pagination to iterate over the response')
101: ->bindTo($this->Paginate),
102: CliOption::build()
103: ->long('stream')
104: ->short('s')
105: ->description('Output a stream of entities when pagination is used')
106: ->bindTo($this->Stream),
107: ];
108: }
109:
110: yield from $this->getGlobalOptionList();
111: }
112:
113: protected function run(string ...$args)
114: {
115: $this->startRun();
116:
117: if ($this->HttpProviders) {
118: $provider = $this->HttpProviders[$this->ProviderBasename];
119: } else {
120: $provider = $this->Provider;
121:
122: if (!is_a(
123: $this->App->getName($provider),
124: HttpSyncProvider::class,
125: true,
126: )) {
127: throw new CliInvalidArgumentsException(sprintf(
128: '%s does not inherit %s',
129: $provider,
130: HttpSyncProvider::class,
131: ));
132: }
133:
134: if (!$this->App->has($provider)) {
135: $this->App->singleton($provider);
136: }
137: }
138:
139: $provider = $this->App->get($provider);
140:
141: try {
142: $query = Get::filter($this->Query);
143: } catch (InvalidArgumentException $ex) {
144: throw new CliInvalidArgumentsException(sprintf(
145: 'invalid query (%s)',
146: $ex->getMessage(),
147: ));
148: }
149:
150: $data = $this->Data !== null
151: ? $this->getJson($this->Data, false)
152: : null;
153:
154: $curler = $provider->getCurler($this->Endpoint);
155: if ($this->Paginate && $curler->getPager() === null) {
156: throw new CliInvalidArgumentsException(sprintf(
157: '%s does not support pagination',
158: $provider->getName(),
159: ));
160: }
161:
162: switch ($this->getMethod()) {
163: case Method::HEAD:
164: $result = $curler->head($query);
165: break;
166:
167: case Method::GET:
168: $result = $this->Paginate
169: ? $curler->getP($query)
170: : $curler->get($query);
171: break;
172:
173: case Method::POST:
174: $result = $this->Paginate
175: ? $curler->postP($data, $query)
176: : $curler->post($data, $query);
177: break;
178:
179: case Method::PUT:
180: $result = $curler->put($data, $query);
181: break;
182:
183: case Method::DELETE:
184: $result = $curler->delete($data, $query);
185: break;
186:
187: case Method::PATCH:
188: $result = $curler->patch($data, $query);
189: break;
190: }
191:
192: if (!$this->Paginate) {
193: echo Json::prettyPrint($result) . \PHP_EOL;
194: return;
195: }
196:
197: /** @var iterable<mixed> $result */
198: $count = 0;
199:
200: if ($this->Stream) {
201: foreach ($result as $entity) {
202: $count++;
203: echo Json::prettyPrint($entity) . \PHP_EOL;
204: }
205: } else {
206: $indent = ' ';
207: foreach ($result as $entity) {
208: if (!$count++) {
209: echo '[' . \PHP_EOL;
210: } else {
211: echo ',' . \PHP_EOL;
212: }
213: echo $indent . Json::prettyPrint($entity, 0, \PHP_EOL . $indent);
214: }
215: if ($count) {
216: echo \PHP_EOL . ']' . \PHP_EOL;
217: } else {
218: echo '[]' . \PHP_EOL;
219: }
220: }
221:
222: Console::summary(Inflect::format(
223: $count,
224: '{{#}} {{#:entity}} retrieved',
225: ));
226: }
227:
228: /**
229: * @return Method::HEAD|Method::GET|Method::POST|Method::PUT|Method::DELETE|Method::PATCH
230: */
231: private function getMethod(): string
232: {
233: if (isset($this->Method)) {
234: return $this->Method;
235: }
236:
237: $method = Str::upper((string) Arr::last($this->getNameParts()));
238: if (!(
239: $method === Method::HEAD
240: || $method === Method::GET
241: || $method === Method::POST
242: || $method === Method::PUT
243: || $method === Method::DELETE
244: || $method === Method::PATCH
245: )) {
246: throw new LogicException(sprintf('Invalid method: %s', $method));
247: }
248:
249: return $this->Method = $method;
250: }
251: }
252: