1: | <?php declare(strict_types=1); |
2: | |
3: | namespace Salient\Sync; |
4: | |
5: | use Salient\Contract\Core\Entity\Readable; |
6: | use Salient\Contract\Core\Pipeline\PipelineInterface; |
7: | use Salient\Contract\Core\ArrayMapperInterface; |
8: | use Salient\Contract\Core\Chainable; |
9: | use Salient\Contract\Core\ListConformity; |
10: | use Salient\Contract\Iterator\FluentIteratorInterface; |
11: | use Salient\Contract\Sync\EntitySource; |
12: | use Salient\Contract\Sync\FilterPolicy; |
13: | use Salient\Contract\Sync\SyncContextInterface; |
14: | use Salient\Contract\Sync\SyncDefinitionInterface; |
15: | use Salient\Contract\Sync\SyncEntityInterface; |
16: | use Salient\Contract\Sync\SyncOperation as OP; |
17: | use Salient\Contract\Sync\SyncProviderInterface; |
18: | use Salient\Core\Concern\HasChainableMethods; |
19: | use Salient\Core\Concern\HasMutator; |
20: | use Salient\Core\Concern\HasReadableProperties; |
21: | use Salient\Core\Pipeline; |
22: | use Salient\Iterator\IterableIterator; |
23: | use Salient\Sync\Exception\SyncEntityNotFoundException; |
24: | use Salient\Sync\Reflection\ReflectionSyncEntity; |
25: | use Salient\Sync\Reflection\ReflectionSyncProvider; |
26: | use Salient\Sync\Support\SyncIntrospector; |
27: | use Salient\Sync\Support\SyncPipelineArgument; |
28: | use Salient\Utility\Reflect; |
29: | use Closure; |
30: | use LogicException; |
31: | |
32: | /** |
33: | * @phpstan-type SyncOperationClosure (Closure(SyncContextInterface, int|string|null, mixed...): TEntity)|(Closure(SyncContextInterface, mixed...): iterable<array-key,TEntity>)|(Closure(SyncContextInterface, TEntity, mixed...): TEntity)|(Closure(SyncContextInterface, iterable<TEntity>, mixed...): iterable<array-key,TEntity>) |
34: | * @phpstan-type OverrideClosure (Closure(static, OP::*, SyncContextInterface, int|string|null, mixed...): TEntity)|(Closure(static, OP::*, SyncContextInterface, mixed...): iterable<array-key,TEntity>)|(Closure(static, OP::*, SyncContextInterface, TEntity, mixed...): TEntity)|(Closure(static, OP::*, SyncContextInterface, iterable<TEntity>, mixed...): iterable<array-key,TEntity>) |
35: | * |
36: | * @property-read class-string<TEntity> $Entity The entity being serviced |
37: | * @property-read TProvider $Provider The provider servicing the entity |
38: | * @property-read array<OP::*> $Operations Supported sync operations |
39: | * @property-read ListConformity::* $Conformity Conformity level of data returned by the provider for this entity |
40: | * @property-read FilterPolicy::* $FilterPolicy Action to take when filters are not claimed by the provider |
41: | * @property-read array<OP::*,Closure(SyncDefinitionInterface<TEntity,TProvider>, OP::*, SyncContextInterface, mixed...): (iterable<array-key,TEntity>|TEntity)> $Overrides Array that maps sync operations to closures that override other implementations |
42: | * @phpstan-property-read array<OP::*,OverrideClosure> $Overrides |
43: | * @property-read array<array-key,array-key|array-key[]>|null $KeyMap Array that maps keys to properties for entity data returned by the provider |
44: | * @property-read int-mask-of<ArrayMapperInterface::*> $KeyMapFlags Array mapper flags used if a key map is provided |
45: | * @property-read PipelineInterface<mixed[],TEntity,SyncPipelineArgument>|null $PipelineFromBackend Pipeline that maps provider data to a serialized entity, or `null` if mapping is not required |
46: | * @property-read PipelineInterface<TEntity,mixed[],SyncPipelineArgument>|null $PipelineToBackend Pipeline that maps a serialized entity to provider data, or `null` if mapping is not required |
47: | * @property-read bool $ReadFromList Perform READ operations by iterating over entities returned by READ_LIST |
48: | * @property-read EntitySource::*|null $ReturnEntitiesFrom Source of entity data for the return value of a successful CREATE, UPDATE or DELETE operation |
49: | * |
50: | * @template TEntity of SyncEntityInterface |
51: | * @template TProvider of SyncProviderInterface |
52: | * |
53: | * @implements SyncDefinitionInterface<TEntity,TProvider> |
54: | */ |
55: | abstract class AbstractSyncDefinition implements SyncDefinitionInterface, Chainable, Readable |
56: | { |
57: | use HasChainableMethods; |
58: | use HasMutator; |
59: | use HasReadableProperties; |
60: | |
61: | /** |
62: | * Get a closure to perform a sync operation on the entity |
63: | * |
64: | * This method is called if: |
65: | * |
66: | * - the operation is in {@see AbstractSyncDefinition::$Operations}, |
67: | * - there is no override for the operation, and |
68: | * - the provider has not implemented the operation via a declared method |
69: | * |
70: | * @param OP::* $operation |
71: | * @return (Closure(SyncContextInterface, mixed...): (iterable<array-key,TEntity>|TEntity))|null |
72: | * @phpstan-return ( |
73: | * $operation is OP::READ |
74: | * ? (Closure(SyncContextInterface, int|string|null, mixed...): TEntity) |
75: | * : ( |
76: | * $operation is OP::READ_LIST |
77: | * ? (Closure(SyncContextInterface, mixed...): iterable<array-key,TEntity>) |
78: | * : ( |
79: | * $operation is OP::CREATE|OP::UPDATE|OP::DELETE |
80: | * ? (Closure(SyncContextInterface, TEntity, mixed...): TEntity) |
81: | * : (Closure(SyncContextInterface, iterable<TEntity>, mixed...): iterable<array-key,TEntity>) |
82: | * ) |
83: | * ) |
84: | * )|null |
85: | */ |
86: | abstract protected function getClosure(int $operation): ?Closure; |
87: | |
88: | /** |
89: | * The entity being serviced |
90: | * |
91: | * @var class-string<TEntity> |
92: | */ |
93: | protected string $Entity; |
94: | |
95: | /** |
96: | * The provider servicing the entity |
97: | * |
98: | * @var TProvider |
99: | */ |
100: | protected SyncProviderInterface $Provider; |
101: | |
102: | /** |
103: | * Supported sync operations |
104: | * |
105: | * @var array<OP::*> |
106: | */ |
107: | protected array $Operations; |
108: | |
109: | /** |
110: | * Conformity level of data returned by the provider for this entity |
111: | * |
112: | * Use {@see ListConformity::COMPLETE} or {@see ListConformity::PARTIAL} |
113: | * wherever possible to improve performance. |
114: | * |
115: | * @var ListConformity::* |
116: | */ |
117: | protected $Conformity; |
118: | |
119: | /** |
120: | * Action to take when filters are not claimed by the provider |
121: | * |
122: | * To prevent a request for entities that meet one or more criteria |
123: | * inadvertently reaching the backend as a request for a larger set of |
124: | * entities--if not all of them--the default policy if there are unclaimed |
125: | * filters is {@see FilterPolicy::THROW_EXCEPTION}. |
126: | * |
127: | * @see SyncContextInterface::withOperation() |
128: | * |
129: | * @var FilterPolicy::* |
130: | */ |
131: | protected int $FilterPolicy; |
132: | |
133: | /** |
134: | * Array that maps sync operations to closures that override other |
135: | * implementations |
136: | * |
137: | * Two arguments are inserted before the operation's arguments: |
138: | * |
139: | * - The sync definition object |
140: | * - The sync operation |
141: | * |
142: | * Operations implemented here are added to |
143: | * {@see AbstractSyncDefinition::$Operations} automatically. |
144: | * |
145: | * @var array<OP::*,Closure(SyncDefinitionInterface<TEntity,TProvider>, OP::*, SyncContextInterface, mixed...): (iterable<array-key,TEntity>|TEntity)> |
146: | * @phpstan-var array<OP::*,OverrideClosure> |
147: | */ |
148: | protected array $Overrides = []; |
149: | |
150: | /** |
151: | * Array that maps keys to properties for entity data returned by the |
152: | * provider |
153: | * |
154: | * Providing a key map has the same effect as passing the following pipeline |
155: | * to `$pipelineFromBackend`: |
156: | * |
157: | * ```php |
158: | * <?php |
159: | * Pipeline::create()->throughKeyMap($keyMap); |
160: | * ``` |
161: | * |
162: | * @var array<array-key,array-key|array-key[]>|null |
163: | */ |
164: | protected ?array $KeyMap; |
165: | |
166: | /** |
167: | * Array mapper flags used if a key map is provided |
168: | * |
169: | * @var int-mask-of<ArrayMapperInterface::*> |
170: | */ |
171: | protected int $KeyMapFlags; |
172: | |
173: | /** |
174: | * Pipeline that maps provider data to a serialized entity, or `null` if |
175: | * mapping is not required |
176: | * |
177: | * @var PipelineInterface<mixed[],TEntity,SyncPipelineArgument>|null |
178: | */ |
179: | protected ?PipelineInterface $PipelineFromBackend; |
180: | |
181: | /** |
182: | * Pipeline that maps a serialized entity to provider data, or `null` if |
183: | * mapping is not required |
184: | * |
185: | * @var PipelineInterface<TEntity,mixed[],SyncPipelineArgument>|null |
186: | */ |
187: | protected ?PipelineInterface $PipelineToBackend; |
188: | |
189: | /** |
190: | * Perform READ operations by iterating over entities returned by READ_LIST |
191: | * |
192: | * Useful with backends that don't provide an endpoint for retrieval of |
193: | * individual entities. |
194: | */ |
195: | protected bool $ReadFromList; |
196: | |
197: | /** |
198: | * Source of entity data for the return value of a successful CREATE, UPDATE |
199: | * or DELETE operation |
200: | * |
201: | * @var EntitySource::*|null |
202: | */ |
203: | protected ?int $ReturnEntitiesFrom; |
204: | |
205: | /** @var ReflectionSyncEntity<TEntity> */ |
206: | protected ReflectionSyncEntity $EntityReflector; |
207: | /** @var ReflectionSyncProvider<TProvider> */ |
208: | protected ReflectionSyncProvider $ProviderReflector; |
209: | /** @var array<OP::*,SyncOperationClosure|null> */ |
210: | private array $Closures = []; |
211: | /** @var static|null */ |
212: | private ?self $WithoutOverrides = null; |
213: | |
214: | /** |
215: | * @param class-string<TEntity> $entity |
216: | * @param TProvider $provider |
217: | * @param array<OP::*> $operations |
218: | * @param ListConformity::* $conformity |
219: | * @param FilterPolicy::*|null $filterPolicy |
220: | * @param array<int-mask-of<OP::*>,Closure(SyncDefinitionInterface<TEntity,TProvider>, OP::*, SyncContextInterface, mixed...): (iterable<array-key,TEntity>|TEntity)> $overrides |
221: | * @phpstan-param array<int-mask-of<OP::*>,OverrideClosure> $overrides |
222: | * @param array<array-key,array-key|array-key[]>|null $keyMap |
223: | * @param int-mask-of<ArrayMapperInterface::*> $keyMapFlags |
224: | * @param PipelineInterface<mixed[],TEntity,SyncPipelineArgument>|null $pipelineFromBackend |
225: | * @param PipelineInterface<TEntity,mixed[],SyncPipelineArgument>|null $pipelineToBackend |
226: | * @param EntitySource::*|null $returnEntitiesFrom |
227: | */ |
228: | public function __construct( |
229: | string $entity, |
230: | SyncProviderInterface $provider, |
231: | array $operations = [], |
232: | int $conformity = ListConformity::NONE, |
233: | ?int $filterPolicy = null, |
234: | array $overrides = [], |
235: | ?array $keyMap = null, |
236: | int $keyMapFlags = ArrayMapperInterface::ADD_UNMAPPED, |
237: | ?PipelineInterface $pipelineFromBackend = null, |
238: | ?PipelineInterface $pipelineToBackend = null, |
239: | bool $readFromList = false, |
240: | ?int $returnEntitiesFrom = null |
241: | ) { |
242: | $this->Entity = $entity; |
243: | $this->Provider = $provider; |
244: | $this->Conformity = $conformity; |
245: | $this->FilterPolicy = $filterPolicy |
246: | ?? $provider->getFilterPolicy() ?? FilterPolicy::THROW_EXCEPTION; |
247: | $this->KeyMap = $keyMap; |
248: | $this->KeyMapFlags = $keyMapFlags; |
249: | $this->PipelineFromBackend = $pipelineFromBackend; |
250: | $this->PipelineToBackend = $pipelineToBackend; |
251: | $this->ReadFromList = $readFromList; |
252: | $this->ReturnEntitiesFrom = $returnEntitiesFrom; |
253: | |
254: | /** @var list<int&OP::*> */ |
255: | $allOps = array_values(Reflect::getConstants(OP::class)); |
256: | |
257: | // Expand $overrides into an entry per operation |
258: | foreach ($overrides as $ops => $override) { |
259: | foreach ($allOps as $op) { |
260: | if (!($ops & $op)) { |
261: | continue; |
262: | } |
263: | if (array_key_exists($op, $this->Overrides)) { |
264: | throw new LogicException(sprintf( |
265: | 'Too many overrides for SyncOperation::%s on %s: %s', |
266: | Reflect::getConstantName(OP::class, $op), |
267: | $entity, |
268: | get_class($provider), |
269: | )); |
270: | } |
271: | $this->Overrides[$op] = $override; |
272: | $operations[] = $op; |
273: | } |
274: | } |
275: | |
276: | $this->Operations = array_intersect($allOps, $operations); |
277: | $this->EntityReflector = new ReflectionSyncEntity($entity); |
278: | $this->ProviderReflector = new ReflectionSyncProvider($provider); |
279: | } |
280: | |
281: | /** |
282: | * @internal |
283: | */ |
284: | public function __clone() |
285: | { |
286: | $this->Closures = []; |
287: | $this->WithoutOverrides = null; |
288: | } |
289: | |
290: | /** |
291: | * Get an instance with the given entity data conformity level |
292: | * |
293: | * @param ListConformity::* $conformity |
294: | * @return static |
295: | */ |
296: | final public function withConformity($conformity) |
297: | { |
298: | return $this->with('Conformity', $conformity); |
299: | } |
300: | |
301: | /** |
302: | * Get an instance with the given unclaimed filter policy |
303: | * |
304: | * @param FilterPolicy::* $policy |
305: | * @return static |
306: | */ |
307: | final public function withFilterPolicy(int $policy) |
308: | { |
309: | return $this->with('FilterPolicy', $policy); |
310: | } |
311: | |
312: | /** |
313: | * Get an instance that maps keys to the given properties for entity data |
314: | * returned by the provider |
315: | * |
316: | * @param array<array-key,array-key|array-key[]>|null $map |
317: | * @return static |
318: | */ |
319: | final public function withKeyMap(?array $map) |
320: | { |
321: | return $this->with('KeyMap', $map); |
322: | } |
323: | |
324: | /** |
325: | * Get an instance where the given array mapper flags are used if a key map |
326: | * is provided |
327: | * |
328: | * @param int-mask-of<ArrayMapperInterface::*> $flags |
329: | * @return static |
330: | */ |
331: | final public function withKeyMapFlags(int $flags) |
332: | { |
333: | return $this->with('KeyMapFlags', $flags); |
334: | } |
335: | |
336: | /** |
337: | * Get an instance that uses the given pipeline to map provider data to a |
338: | * serialized entity |
339: | * |
340: | * @param PipelineInterface<mixed[],TEntity,SyncPipelineArgument>|null $pipeline |
341: | * @return static |
342: | */ |
343: | final public function withPipelineFromBackend(?PipelineInterface $pipeline) |
344: | { |
345: | return $this->with('PipelineFromBackend', $pipeline); |
346: | } |
347: | |
348: | /** |
349: | * Get an instance that uses the given pipeline to map a serialized entity |
350: | * to provider data |
351: | * |
352: | * @param PipelineInterface<TEntity,mixed[],SyncPipelineArgument>|null $pipeline |
353: | * @return static |
354: | */ |
355: | final public function withPipelineToBackend(?PipelineInterface $pipeline) |
356: | { |
357: | return $this->with('PipelineToBackend', $pipeline); |
358: | } |
359: | |
360: | /** |
361: | * Get an instance that performs READ operations by iterating over entities |
362: | * returned by READ_LIST |
363: | * |
364: | * @return static |
365: | */ |
366: | final public function withReadFromList(bool $readFromList = true) |
367: | { |
368: | return $this->with('ReadFromList', $readFromList); |
369: | } |
370: | |
371: | /** |
372: | * Get an instance that uses the given entity data source for the return |
373: | * value of a successful CREATE, UPDATE or DELETE operation |
374: | * |
375: | * @param EntitySource::*|null $source |
376: | * @return static |
377: | */ |
378: | final public function withReturnEntitiesFrom(?int $source) |
379: | { |
380: | return $this->with('ReturnEntitiesFrom', $source); |
381: | } |
382: | |
383: | /** |
384: | * @inheritDoc |
385: | */ |
386: | final public function getOperationClosure(int $operation): ?Closure |
387: | { |
388: | // Return a previous result if possible |
389: | if (array_key_exists($operation, $this->Closures)) { |
390: | return $this->Closures[$operation]; |
391: | } |
392: | |
393: | // Overrides take precedence over everything else, including declared |
394: | // methods |
395: | if (array_key_exists($operation, $this->Overrides)) { |
396: | /** @var SyncOperationClosure */ |
397: | // @phpstan-ignore varTag.nativeType |
398: | $closure = fn(SyncContextInterface $ctx, ...$args) => |
399: | $this->Overrides[$operation]( |
400: | $this, |
401: | $operation, |
402: | $ctx, |
403: | ...$args |
404: | ); |
405: | return $this->Closures[$operation] = $closure; |
406: | } |
407: | |
408: | // If a method has been declared for this operation, use it, even if |
409: | // it's not in $this->Operations |
410: | $closure = $this->ProviderReflector->getSyncOperationClosure( |
411: | $operation, |
412: | $this->EntityReflector, |
413: | $this->Provider |
414: | ); |
415: | |
416: | if ($closure) { |
417: | /** @var SyncOperationClosure */ |
418: | // @phpstan-ignore varTag.nativeType |
419: | $closure = fn(SyncContextInterface $ctx, ...$args) => |
420: | $closure( |
421: | $ctx, |
422: | ...$args |
423: | ); |
424: | return $this->Closures[$operation] = $closure; |
425: | } |
426: | |
427: | if ( |
428: | $operation === OP::READ |
429: | && $this->ReadFromList |
430: | && ($closure = $this->getOperationClosure(OP::READ_LIST)) |
431: | ) { |
432: | return $this->Closures[$operation] = |
433: | function (SyncContextInterface $ctx, $id, ...$args) use ($closure) { |
434: | $entity = $this |
435: | ->getFluentIterator($closure($ctx, ...$args)) |
436: | ->nextWithValue('Id', $id); |
437: | if ($entity === null) { |
438: | throw new SyncEntityNotFoundException($this->Provider, $this->Entity, $id); |
439: | } |
440: | return $entity; |
441: | }; |
442: | } |
443: | |
444: | // Return null if the operation doesn't appear in $this->Operations |
445: | if (!in_array($operation, $this->Operations, true)) { |
446: | return $this->Closures[$operation] = null; |
447: | } |
448: | |
449: | // Otherwise, get a closure from the subclass |
450: | return $this->Closures[$operation] = $this->getClosure($operation); |
451: | } |
452: | |
453: | /** |
454: | * Ignoring overrides, get a closure to perform a sync operation on the |
455: | * entity, throwing an exception if the operation is not supported |
456: | * |
457: | * @param OP::* $operation |
458: | * @return ( |
459: | * $operation is OP::READ |
460: | * ? (Closure(SyncContextInterface, int|string|null, mixed...): TEntity) |
461: | * : ( |
462: | * $operation is OP::READ_LIST |
463: | * ? (Closure(SyncContextInterface, mixed...): iterable<array-key,TEntity>) |
464: | * : ( |
465: | * $operation is OP::CREATE|OP::UPDATE|OP::DELETE |
466: | * ? (Closure(SyncContextInterface, TEntity, mixed...): TEntity) |
467: | * : (Closure(SyncContextInterface, iterable<TEntity>, mixed...): iterable<array-key,TEntity>) |
468: | * ) |
469: | * ) |
470: | * ) |
471: | * @throws LogicException If the operation is not supported. |
472: | */ |
473: | final public function getFallbackClosure(int $operation): Closure |
474: | { |
475: | $closure = ($this->WithoutOverrides ??= $this->with('Overrides', [])) |
476: | ->getOperationClosure($operation); |
477: | |
478: | if ($closure === null) { |
479: | throw new LogicException(sprintf( |
480: | 'SyncOperation::%s not supported on %s', |
481: | Reflect::getConstantName(OP::class, $operation), |
482: | $this->Entity, |
483: | )); |
484: | } |
485: | |
486: | return $closure; |
487: | } |
488: | |
489: | /** |
490: | * Get an entity-to-data pipeline for the entity |
491: | * |
492: | * Before returning the pipeline: |
493: | * |
494: | * - a pipe that serializes any unserialized {@see SyncEntityInterface} |
495: | * instances is added via {@see PipelineInterface::through()} |
496: | * |
497: | * @return PipelineInterface<TEntity,mixed[],SyncPipelineArgument> |
498: | */ |
499: | final protected function getPipelineToBackend(): PipelineInterface |
500: | { |
501: | /** @var PipelineInterface<TEntity,mixed[],SyncPipelineArgument> */ |
502: | $pipeline = $this->PipelineToBackend ?? Pipeline::create(); |
503: | |
504: | /** @var PipelineInterface<TEntity,mixed[],SyncPipelineArgument> */ |
505: | $pipeline = $pipeline->through( |
506: | fn($payload, Closure $next) => |
507: | $payload instanceof SyncEntityInterface |
508: | ? $next($payload->toArray()) |
509: | : $next($payload) |
510: | ); |
511: | |
512: | return $pipeline; |
513: | } |
514: | |
515: | /** |
516: | * Get a data-to-entity pipeline for the entity |
517: | * |
518: | * Before returning the pipeline: |
519: | * |
520: | * - if the definition has a key map, it is applied via |
521: | * {@see PipelineInterface::throughKeyMap()} |
522: | * - a closure to create instances of the entity from arrays returned by the |
523: | * pipeline is applied via {@see PipelineInterface::then()} |
524: | * |
525: | * @return PipelineInterface<mixed[],TEntity,SyncPipelineArgument> |
526: | */ |
527: | final protected function getPipelineFromBackend(): PipelineInterface |
528: | { |
529: | /** @var PipelineInterface<mixed[],TEntity,SyncPipelineArgument> */ |
530: | $pipeline = $this->PipelineFromBackend ?? Pipeline::create(); |
531: | |
532: | if ($this->KeyMap !== null) { |
533: | $pipeline = $pipeline->throughKeyMap($this->KeyMap, $this->KeyMapFlags); |
534: | } |
535: | |
536: | /** @var SyncPipelineArgument|null */ |
537: | $currentArg = null; |
538: | /** @var SyncContextInterface|null */ |
539: | $ctx = null; |
540: | |
541: | return $pipeline |
542: | ->then(function ( |
543: | array $data, |
544: | PipelineInterface $pipeline, |
545: | SyncPipelineArgument $arg |
546: | ) use (&$ctx, &$closure, &$currentArg) { |
547: | if (!$ctx || !$closure || $currentArg !== $arg) { |
548: | $ctx = $arg->Context->withConformity($this->Conformity); |
549: | $closure = in_array( |
550: | $this->Conformity, |
551: | [ListConformity::PARTIAL, ListConformity::COMPLETE] |
552: | ) |
553: | ? SyncIntrospector::getService($ctx->getContainer(), $this->Entity) |
554: | ->getCreateSyncEntityFromSignatureClosure(array_keys($data)) |
555: | : SyncIntrospector::getService($ctx->getContainer(), $this->Entity) |
556: | ->getCreateSyncEntityFromClosure(); |
557: | $currentArg = $arg; |
558: | } |
559: | /** @var TEntity */ |
560: | $entity = $closure($data, $this->Provider, $ctx); |
561: | |
562: | return $entity; |
563: | }); |
564: | } |
565: | |
566: | /** |
567: | * @param iterable<TEntity> $result |
568: | * @return FluentIteratorInterface<array-key,TEntity> |
569: | */ |
570: | private function getFluentIterator(iterable $result): FluentIteratorInterface |
571: | { |
572: | if (!$result instanceof FluentIteratorInterface) { |
573: | return IterableIterator::fromValues($result); |
574: | } |
575: | |
576: | return $result; |
577: | } |
578: | |
579: | /** |
580: | * @inheritDoc |
581: | */ |
582: | public static function getReadableProperties(): array |
583: | { |
584: | return [ |
585: | 'Entity', |
586: | 'Provider', |
587: | 'Operations', |
588: | 'Conformity', |
589: | 'FilterPolicy', |
590: | 'Overrides', |
591: | 'KeyMap', |
592: | 'KeyMapFlags', |
593: | 'PipelineFromBackend', |
594: | 'PipelineToBackend', |
595: | 'ReadFromList', |
596: | 'ReturnEntitiesFrom', |
597: | ]; |
598: | } |
599: | } |
600: |