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\Sync\Exception\SyncEntityNotFoundExceptionInterface; |
9: | use Salient\Contract\Sync\DeferralPolicy; |
10: | use Salient\Contract\Sync\HydrationPolicy; |
11: | use Salient\Contract\Sync\SyncEntityInterface; |
12: | use Salient\Contract\Sync\SyncProviderInterface; |
13: | use Salient\Core\Facade\Console; |
14: | use Salient\Sync\SyncSerializeRules; |
15: | use Salient\Sync\SyncUtil; |
16: | use Salient\Utility\Arr; |
17: | use Salient\Utility\File; |
18: | use Salient\Utility\Get; |
19: | use Salient\Utility\Inflect; |
20: | use Salient\Utility\Json; |
21: | use InvalidArgumentException; |
22: | |
23: | |
24: | |
25: | |
26: | |
27: | |
28: | |
29: | |
30: | final class GetSyncEntity extends AbstractSyncCommand |
31: | { |
32: | private string $EntityBasename = ''; |
33: | |
34: | private string $Entity = SyncEntityInterface::class; |
35: | private ?string $EntityId = null; |
36: | private ?string $ProviderBasename = null; |
37: | |
38: | private string $Provider = SyncProviderInterface::class; |
39: | |
40: | private array $Filter = []; |
41: | private bool $Shallow = false; |
42: | private bool $IncludeCanonicalId = false; |
43: | private bool $IncludeMeta = false; |
44: | private bool $Stream = false; |
45: | |
46: | private array $Field = []; |
47: | private bool $Csv = false; |
48: | |
49: | |
50: | |
51: | |
52: | private array $Fields; |
53: | |
54: | public function getDescription(): string |
55: | { |
56: | return 'Get entities from ' . ( |
57: | $this->Entities |
58: | ? 'a registered provider' |
59: | : 'a provider' |
60: | ); |
61: | } |
62: | |
63: | protected function getOptionList(): iterable |
64: | { |
65: | $entityBuilder = CliOption::build() |
66: | ->name('entity') |
67: | ->required(); |
68: | |
69: | $entityIdBuilder = CliOption::build() |
70: | ->name('entity_id') |
71: | ->description(<<<EOF |
72: | The unique identifier of an entity to request |
73: | |
74: | If not given, a list of entities is requested. |
75: | EOF) |
76: | ->optionType(CliOptionType::VALUE_POSITIONAL) |
77: | ->bindTo($this->EntityId); |
78: | |
79: | $providerBuilder = CliOption::build() |
80: | ->long('provider') |
81: | ->short('p') |
82: | ->valueName('provider'); |
83: | |
84: | if ($this->Entities) { |
85: | yield from [ |
86: | $entityBuilder |
87: | ->optionType(CliOptionType::ONE_OF_POSITIONAL) |
88: | ->allowedValues(array_keys($this->Entities)) |
89: | ->bindTo($this->EntityBasename), |
90: | $entityIdBuilder, |
91: | $providerBuilder |
92: | ->description(<<<EOF |
93: | The provider to request entities from |
94: | |
95: | If not given, the entity's default provider is used. |
96: | EOF) |
97: | ->optionType(CliOptionType::ONE_OF) |
98: | ->allowedValues(array_keys($this->EntityProviders)) |
99: | ->bindTo($this->ProviderBasename), |
100: | ]; |
101: | } else { |
102: | yield from [ |
103: | $entityBuilder |
104: | ->description('The fully-qualified name of the entity to request') |
105: | ->optionType(CliOptionType::VALUE_POSITIONAL) |
106: | ->bindTo($this->Entity), |
107: | $entityIdBuilder, |
108: | $providerBuilder |
109: | ->description('The fully-qualified name of the provider to request entities from') |
110: | ->optionType(CliOptionType::VALUE) |
111: | ->required() |
112: | ->bindTo($this->Provider), |
113: | ]; |
114: | } |
115: | |
116: | yield from [ |
117: | CliOption::build() |
118: | ->long('filter') |
119: | ->short('f') |
120: | ->valueName('term=value') |
121: | ->description('Apply a filter to the request') |
122: | ->optionType(CliOptionType::VALUE) |
123: | ->multipleAllowed() |
124: | ->bindTo($this->Filter), |
125: | CliOption::build() |
126: | ->long('shallow') |
127: | ->description('Do not resolve entity relationships') |
128: | ->bindTo($this->Shallow), |
129: | CliOption::build() |
130: | ->long('include-canonical-id') |
131: | ->short('I') |
132: | ->description('Include canonical_id in the output') |
133: | ->bindTo($this->IncludeCanonicalId), |
134: | CliOption::build() |
135: | ->long('include-meta') |
136: | ->short('M') |
137: | ->description('Include meta values in the output') |
138: | ->bindTo($this->IncludeMeta), |
139: | CliOption::build() |
140: | ->long('stream') |
141: | ->short('s') |
142: | ->description('Output a stream of entities') |
143: | ->bindTo($this->Stream), |
144: | CliOption::build() |
145: | ->long('field') |
146: | ->short('F') |
147: | ->valueName('(<field>|<field>=<title>)') |
148: | ->description('Limit output to the given fields, e.g. "id,user.id=user id,title"') |
149: | ->optionType(CliOptionType::VALUE) |
150: | ->multipleAllowed() |
151: | ->bindTo($this->Field), |
152: | CliOption::build() |
153: | ->long('csv') |
154: | ->short('c') |
155: | ->description('Generate CSV output (implies `--shallow` if `--field` is not given)') |
156: | ->bindTo($this->Csv), |
157: | ]; |
158: | |
159: | yield from $this->getGlobalOptionList(); |
160: | } |
161: | |
162: | protected function run(string ...$args) |
163: | { |
164: | $this->startRun(); |
165: | |
166: | $this->Fields = []; |
167: | |
168: | if ($this->Entities) { |
169: | |
170: | $entity = $this->Entities[$this->EntityBasename]; |
171: | |
172: | $provider = $this->ProviderBasename ?? array_search( |
173: | $this->App->getName(SyncUtil::getEntityTypeProvider($entity, SyncUtil::getStore($this->App))), |
174: | $this->Providers, |
175: | ); |
176: | |
177: | if ($provider === false) { |
178: | throw new CliInvalidArgumentsException( |
179: | sprintf('no default provider: %s', $entity) |
180: | ); |
181: | } |
182: | |
183: | $provider = $this->Providers[$provider]; |
184: | } else { |
185: | |
186: | $entity = $this->Entity; |
187: | $provider = $this->Provider; |
188: | |
189: | if (!is_a( |
190: | $this->App->getName($entity), |
191: | SyncEntityInterface::class, |
192: | true |
193: | )) { |
194: | throw new CliInvalidArgumentsException(sprintf( |
195: | '%s does not implement %s', |
196: | $entity, |
197: | SyncEntityInterface::class, |
198: | )); |
199: | } |
200: | |
201: | if (!is_a( |
202: | $this->App->getName($provider), |
203: | SyncProviderInterface::class, |
204: | true |
205: | )) { |
206: | throw new CliInvalidArgumentsException(sprintf( |
207: | '%s does not implement %s', |
208: | $provider, |
209: | SyncProviderInterface::class, |
210: | )); |
211: | } |
212: | |
213: | if (!$this->App->has($provider)) { |
214: | $this->App->singleton($provider); |
215: | } |
216: | } |
217: | |
218: | $provider = $this->App->get($provider); |
219: | |
220: | $entityProvider = SyncUtil::getEntityTypeProvider($entity, SyncUtil::getStore($this->App)); |
221: | if (!$provider instanceof $entityProvider) { |
222: | throw new CliInvalidArgumentsException(sprintf( |
223: | '%s does not service %s', |
224: | get_class($provider), |
225: | $entity, |
226: | )); |
227: | } |
228: | |
229: | try { |
230: | $filter = Get::filter($this->Filter); |
231: | } catch (InvalidArgumentException $ex) { |
232: | throw new CliInvalidArgumentsException(sprintf( |
233: | 'invalid filter (%s)', |
234: | $ex->getMessage(), |
235: | )); |
236: | } |
237: | |
238: | $context = $provider->getContext(); |
239: | if ($this->Shallow || ($this->Csv && !$this->Field)) { |
240: | $context = $context |
241: | ->withDeferralPolicy(DeferralPolicy::DO_NOT_RESOLVE) |
242: | ->withHydrationPolicy(HydrationPolicy::SUPPRESS); |
243: | } |
244: | |
245: | |
246: | $rules = $entity::getSerializeRules() |
247: | ->withIncludeCanonicalId($this->IncludeCanonicalId) |
248: | ->withIncludeMeta($this->IncludeMeta); |
249: | |
250: | if ($this->Field) { |
251: | foreach ($this->Field as $field) { |
252: | $field = explode('=', $field, 2); |
253: | $this->Fields[$field[0]] = $field[1] ?? $field[0]; |
254: | } |
255: | } |
256: | |
257: | Console::info( |
258: | sprintf('Retrieving from %s:', $provider->getName()), |
259: | $this->Store->getEntityTypeUri($entity) |
260: | . ($this->EntityId === null ? '' : '/' . $this->EntityId) |
261: | ); |
262: | |
263: | if ($this->EntityId === null) { |
264: | $result = $this->Stream |
265: | ? $provider->with($entity, $context)->getList($filter) |
266: | : $provider->with($entity, $context)->getListA($filter); |
267: | } else { |
268: | try { |
269: | $result = $provider->with($entity, $context)->get($this->EntityId, $filter); |
270: | } catch (SyncEntityNotFoundExceptionInterface $ex) { |
271: | throw new CliInvalidArgumentsException( |
272: | sprintf( |
273: | 'entity not found: %s (%s)', |
274: | $this->EntityId, |
275: | $ex->getMessage(), |
276: | ) |
277: | ); |
278: | } |
279: | } |
280: | |
281: | $stdout = Console::getStdoutTarget(); |
282: | $tty = $stdout->isTty(); |
283: | |
284: | if ($this->Csv) { |
285: | if ($this->EntityId !== null) { |
286: | $result = [$result]; |
287: | } |
288: | |
289: | File::writeCsv( |
290: | 'php://output', |
291: | $result, |
292: | true, |
293: | null, |
294: | fn(SyncEntityInterface $entity, int $index) => |
295: | $this->serialize($entity, $rules, !$index), |
296: | $count, |
297: | $tty ? $stdout->getEol() : "\r\n", |
298: | !$tty, |
299: | !$tty, |
300: | ); |
301: | } elseif ($this->Stream && $this->EntityId === null) { |
302: | $eol = $tty ? $stdout->getEol() : \PHP_EOL; |
303: | $count = 0; |
304: | foreach ($result as $entity) { |
305: | echo Json::prettyPrint($this->serialize($entity, $rules, !$count), 0, $eol) . $eol; |
306: | $count++; |
307: | } |
308: | } else { |
309: | if ($this->EntityId !== null) { |
310: | $result = [$result]; |
311: | } |
312: | |
313: | $output = []; |
314: | $count = 0; |
315: | foreach ($result as $entity) { |
316: | $output[] = $this->serialize($entity, $rules, !$count); |
317: | $count++; |
318: | } |
319: | |
320: | if ($this->EntityId !== null) { |
321: | $output = array_shift($output); |
322: | } |
323: | |
324: | $eol = $tty ? $stdout->getEol() : \PHP_EOL; |
325: | echo Json::prettyPrint($output, 0, $eol) . $eol; |
326: | } |
327: | |
328: | Console::summary(Inflect::format( |
329: | $count, |
330: | '{{#}} {{#:entity}} retrieved', |
331: | )); |
332: | } |
333: | |
334: | |
335: | |
336: | |
337: | |
338: | |
339: | private function serialize( |
340: | SyncEntityInterface $entity, |
341: | SyncSerializeRules $rules, |
342: | bool $check = false |
343: | ): array { |
344: | $entity = $entity->toArrayWith($rules); |
345: | if (!$this->Fields) { |
346: | return $entity; |
347: | } |
348: | foreach ($this->Fields as $field => $name) { |
349: | if ($check && !Arr::has($entity, $field)) { |
350: | throw new CliInvalidArgumentsException(sprintf( |
351: | 'Invalid field: %s', |
352: | $field, |
353: | )); |
354: | } |
355: | $result[$name] = Arr::get($entity, $field, null); |
356: | } |
357: | return $result; |
358: | } |
359: | } |
360: | |