-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathPaymentCardFactory.php
More file actions
256 lines (212 loc) · 8.99 KB
/
PaymentCardFactory.php
File metadata and controls
256 lines (212 loc) · 8.99 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
<?php
declare( strict_types = 1 );
namespace TheWebSolver\Codegarage\PaymentCard;
use Generator;
use Throwable;
use TypeError;
use RuntimeException;
use OutOfBoundsException;
use InvalidArgumentException;
use TheWebSolver\Codegarage\PaymentCard\Event\CardCreated;
use TheWebSolver\Codegarage\PaymentCard\Interfaces\CardFactory;
use TheWebSolver\Codegarage\PaymentCard\Interfaces\PaymentCardType;
use TheWebSolver\Codegarage\PaymentCard\Interfaces\CardCreatingAction;
/** @template-implements CardFactory<PaymentCardType> */
class PaymentCardFactory implements CardFactory {
public const DEFAULT_CARD_TYPE = 'Credit Card';
/**
* Possible array keys and their values' datatype Schema for a Payment Card.
*
* - If `type` key not passed, Payment Card is treated as a Credit Card.
* - If `classname` key not passed, base `PaymentCard` class is used.
* - If `checkLuhn` key not passed, Luhn algorithm is always checked.
*/
public const CARD_SCHEMA = [
'type?' => 'string',
'classname?' => 'class-string<' . PaymentCardType::class . '>',
'checkLuhn?' => 'bool',
'name' => 'string',
'alias' => 'string',
'breakpoint' => 'list<int>',
'code' => 'array{name:string,size:int}',
'length' => 'list<int|list<int>>',
'idRange' => 'list<int|list<int>>',
];
/** @placeholder: `%s:` Resource filepath where payload data exists */
public const INVALID_PAYLOAD_PATH = 'Invalid %s provided for creating cards. File must return an array data.';
/** @placeholder: `%s:` Provided payload index */
public const UNDEFINED_PAYLOAD_INDEX = 'Impossible to create Payment Card instance for undefined payload index: "%s".';
/** @placeholder `1:` Index details, `2:` Path details, `3:` JSON encoded args, `4:`, Previous exception msg, `5:` End of line */
public const INVALID_PAYLOAD_SCHEMA = 'Invalid Payment Card arguments given%1$s%2$s.%5$sGiven argument: %3$s%5$sError message: %4$s.';
public const NON_RESOLVABLE_PAYLOAD = 'Unable to resolve payload for creating Card Type. The payload was neither a valid resource path nor a non-empty array of Card Type Schema.';
/** @var non-empty-array<mixed> */
private array $payload;
/** @var non-empty-string */
private string $filePath;
private string $fileType = '';
/** @var ?class-string<PaymentCardType> */
private static ?string $defaultCardClass;
/** @param class-string<PaymentCardType> $classname */
public static function setGlobalCardClass( string $classname ): void {
self::$defaultCardClass ??= $classname;
}
public static function resetGlobalCardClass(): void {
self::$defaultCardClass = null;
}
/**
* @param non-empty-string $path The payload resource path.
* @param list<int|non-empty-string> $indicesToCreate Only payload indices that should create card instance.
* @throws TypeError When $args passed does not match the Payment Card schema.
*/
public static function createFromFile( string $path, array $indicesToCreate = [] ): static {
$factory = new static( payload: [], indicesToCreate: $indicesToCreate );
$factory->filePath = $path;
return $factory;
}
/**
* @param string|mixed[] $payload The payload resource path or an array of Card Schemas array.
* @param list<int|non-empty-string> $indicesToCreate Only payload indices that should create card instance.
*/
final public function __construct( string|array $payload, private readonly array $indicesToCreate = [] ) {
$this->withPayload( $payload );
}
public function getPayload(): array {
return $this->payload;
}
public function getResourcePath(): ?string {
return $this->filePath ?? null;
}
public function getCreatableIndices(): array {
return $this->indicesToCreate;
}
public function create( string|int $payloadIndex ): PaymentCardType {
$this->resolvePayloadContent();
$args = $this->payload[ $payloadIndex ]
?? throw new OutOfBoundsException( sprintf( self::UNDEFINED_PAYLOAD_INDEX, $payloadIndex ) );
try {
return $this->getCardInstance( $args )
->setName( $args['name'] )
->setAlias( $args['alias'] )
->setBreakpoint( ...$args['breakpoint'] )
->setCode( ...$args['code'] )
->setLength( $args['length'] )
->setIdRange( $args['idRange'] );
} catch ( TypeError | InvalidArgumentException $e ) {
$this->shutdownForInvalidSchema( $args, $payloadIndex, $e );
}
}
public function lazyload( ?CardCreatingAction $handler = null ): Generator {
$this->resolvePayloadContent();
$generator = $this->lazyloadSentPayloadIndexOnly();
foreach ( $this->payload as $payloadIndex => $payloadValue ) {
$card = $generator->send( $this->isCreatable( $payloadIndex ) );
$handler?->handle( $event = new CardCreated( $card, $payloadIndex, $payloadValue ) );
yield $payloadIndex => $card;
if ( isset( $event ) && $event->shouldStopPropagation() ) {
return;
}
}
}
/**
* Creates Card instance lazily based on payload index sent and matching it with the current index before yield.
*
* @return Generator<array-key,?PaymentCardType> Returns Card instance or null based on sent value.
* @throws RuntimeException When payload cannot be resolved.
* @see CardFactory::lazyload()
*/
public function lazyloadSentPayloadIndexOnly(): Generator {
$this->resolvePayloadContent();
$card = null;
foreach ( $this->payload as $payloadIndex => $payloadValue ) {
$sent = ( yield $payloadIndex => $card );
$card = ! isset( $sent ) ? $card : $this->maybeCreateForIndex( $sent, $payloadIndex );
}
// The last one is never yielded, so we handle it here.
if ( isset( $sent ) && $sent ) {
yield $payloadIndex => $this->maybeCreateForIndex( $sent, $payloadIndex );
}
}
private function isCreatable( string|int $payloadIndex ): bool {
return ! ( $indices = $this->getCreatableIndices() ) || in_array( $payloadIndex, $indices, strict: true );
}
private function maybeCreateForIndex( mixed $sent, string|int $index ): ?PaymentCardType {
return match ( true ) {
is_string( $sent ), is_int( $sent ) => $sent === $index ? $this->create( $index ) : null,
is_bool( $sent ) => $sent ? $this->create( $index ) : null,
default => null,
};
}
/** @param string|array<mixed> $payload The payload resource path or a Single Card Schema array or an array of Card Schemas array. */
private function withPayload( string|array $payload ): self {
if ( is_string( $payload ) && ! empty( $payload ) ) {
$this->filePath = $payload;
} elseif ( ! empty( $payload ) ) {
$this->payload = $payload;
}
return $this;
}
private function resolvePayloadContent(): void {
$this->payload ??= ! is_array( $content = $this->parseContentFromFilepath() ) || empty( $content )
? throw new RuntimeException( self::NON_RESOLVABLE_PAYLOAD )
: $content;
}
/** @param array<string,mixed> $args */
private function getCardInstance( array $args ): PaymentCardType {
[ $type, $classname, $checkLuhn ] = $this->polyfill( $args );
return new $classname( $type, $checkLuhn );
}
/**
* @param array<string,mixed> $args
* @return array{string,class-string<PaymentCardType>,bool}
*/
private function polyfill( array $args ): array {
$class = $args['classname'] ?? null;
$default = self::$defaultCardClass ?? PaymentCard::class;
return [
is_string( $card = ( $args['type'] ?? null ) ) ? $card : self::DEFAULT_CARD_TYPE,
is_string( $class ) && is_a( $class, PaymentCardType::class, allow_string: true ) ? $class : $default,
is_bool( $luhn = ( $args['checkLuhn'] ?? null ) ) ? $luhn : true,
];
}
private function parseContentFromFilepath(): mixed {
return match ( true ) {
! is_readable( $this->filePath ?? '' ) => null,
str_ends_with( $this->filePath, 'json' ) => $this->parseJsonContent(),
str_ends_with( $this->filePath, 'php' ) => $this->parsePhpContent(),
default => self::invalidFile(
( $this->fileType ? strtoupper( $this->fileType ) . ' ' : '' ) . "file: {$this->filePath}"
),
};
}
private function parsePhpContent(): mixed {
$this->fileType = 'php';
$content = require $this->filePath;
return is_callable( $content ) ? $content() : $content;
}
private function parseJsonContent(): mixed {
$this->fileType = 'json';
return ( false !== $json = file_get_contents( $this->filePath ) )
? json_decode( $json, associative: true )
: self::invalidFile( 'JSON file: ' . $this->filePath );
}
private static function invalidFile( string $typeWithPath ): never {
throw new TypeError( sprintf( self::INVALID_PAYLOAD_PATH, $typeWithPath ) );
}
/**
* @param mixed[] $args
* @throws TypeError When invalid card args given.
*/
private function shutdownForInvalidSchema( array $args, string|int|null $index, Throwable $e ): never {
throw new TypeError(
previous: $e,
message: sprintf(
self::INVALID_PAYLOAD_SCHEMA,
/* 1: */ null !== $index ? ' for array key [#' . $index . ']' : '',
/* 2: */ $this->filePath ? ' in file "' . $this->filePath . '"' : '',
/* 3: */ json_encode( $args ),
/* 4: */ $e->getMessage(),
/* 5: */ PHP_EOL,
)
);
}
}