Total Complexity | 73 |
Total Lines | 471 |
Duplicated Lines | 0 % |
Changes | 0 |
Complex classes like TransactionRequest often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use TransactionRequest, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
38 | class TransactionRequest extends Request |
||
39 | { |
||
40 | /** |
||
41 | * @return bool |
||
42 | */ |
||
43 | public function authorize(): bool |
||
47 | } |
||
48 | |||
49 | /** |
||
50 | * @return array |
||
51 | */ |
||
52 | public function getAll(): array |
||
53 | { |
||
54 | $data = [ |
||
55 | // basic fields for journal: |
||
56 | 'type' => $this->string('type'), |
||
57 | 'date' => $this->date('date'), |
||
58 | 'description' => $this->string('description'), |
||
59 | 'piggy_bank_id' => $this->integer('piggy_bank_id'), |
||
60 | 'piggy_bank_name' => $this->string('piggy_bank_name'), |
||
61 | 'bill_id' => $this->integer('bill_id'), |
||
62 | 'bill_name' => $this->string('bill_name'), |
||
63 | 'tags' => explode(',', $this->string('tags')), |
||
64 | |||
65 | // then, custom fields for journal |
||
66 | 'interest_date' => $this->date('interest_date'), |
||
67 | 'book_date' => $this->date('book_date'), |
||
68 | 'process_date' => $this->date('process_date'), |
||
69 | 'due_date' => $this->date('due_date'), |
||
70 | 'payment_date' => $this->date('payment_date'), |
||
71 | 'invoice_date' => $this->date('invoice_date'), |
||
72 | 'internal_reference' => $this->string('internal_reference'), |
||
73 | 'notes' => $this->string('notes'), |
||
74 | |||
75 | // then, transactions (see below). |
||
76 | 'transactions' => [], |
||
77 | |||
78 | ]; |
||
79 | foreach ($this->get('transactions') as $index => $transaction) { |
||
80 | $array = [ |
||
81 | 'description' => $transaction['description'] ?? null, |
||
82 | 'amount' => $transaction['amount'], |
||
83 | 'currency_id' => isset($transaction['currency_id']) ? (int)$transaction['currency_id'] : null, |
||
84 | 'currency_code' => $transaction['currency_code'] ?? null, |
||
85 | 'foreign_amount' => $transaction['foreign_amount'] ?? null, |
||
86 | 'foreign_currency_id' => isset($transaction['foreign_currency_id']) ? (int)$transaction['foreign_currency_id'] : null, |
||
87 | 'foreign_currency_code' => $transaction['foreign_currency_code'] ?? null, |
||
88 | 'budget_id' => isset($transaction['budget_id']) ? (int)$transaction['budget_id'] : null, |
||
89 | 'budget_name' => $transaction['budget_name'] ?? null, |
||
90 | 'category_id' => isset($transaction['category_id']) ? (int)$transaction['category_id'] : null, |
||
91 | 'category_name' => $transaction['category_name'] ?? null, |
||
92 | 'source_id' => isset($transaction['source_id']) ? (int)$transaction['source_id'] : null, |
||
93 | 'source_name' => isset($transaction['source_name']) ? (string)$transaction['source_name'] : null, |
||
94 | 'destination_id' => isset($transaction['destination_id']) ? (int)$transaction['destination_id'] : null, |
||
95 | 'destination_name' => isset($transaction['destination_name']) ? (string)$transaction['destination_name'] : null, |
||
96 | 'reconciled' => $transaction['reconciled'] ?? false, |
||
97 | 'identifier' => $index, |
||
98 | ]; |
||
99 | $data['transactions'][] = $array; |
||
100 | } |
||
101 | |||
102 | return $data; |
||
103 | } |
||
104 | |||
105 | /** |
||
106 | * @return array |
||
107 | */ |
||
108 | public function rules(): array |
||
161 | |||
162 | |||
163 | } |
||
164 | |||
165 | /** |
||
166 | * Configure the validator instance. |
||
167 | * |
||
168 | * @param Validator $validator |
||
169 | * |
||
170 | * @return void |
||
171 | * @throws \FireflyIII\Exceptions\FireflyException |
||
172 | */ |
||
173 | public function withValidator(Validator $validator): void |
||
174 | { |
||
175 | $validator->after( |
||
176 | function (Validator $validator) { |
||
177 | $this->atLeastOneTransaction($validator); |
||
178 | $this->checkValidDescriptions($validator); |
||
179 | $this->equalToJournalDescription($validator); |
||
180 | $this->emptySplitDescriptions($validator); |
||
181 | $this->foreignCurrencyInformation($validator); |
||
182 | $this->validateAccountInformation($validator); |
||
183 | $this->validateSplitAccounts($validator); |
||
184 | } |
||
185 | ); |
||
186 | } |
||
187 | |||
188 | /** |
||
189 | * Throws an error when this asset account is invalid. |
||
190 | * |
||
191 | * @param Validator $validator |
||
192 | * @param int|null $accountId |
||
193 | * @param null|string $accountName |
||
194 | * @param string $idField |
||
195 | * @param string $nameField |
||
196 | * |
||
197 | * @return null|Account |
||
198 | */ |
||
199 | protected function assetAccountExists(Validator $validator, ?int $accountId, ?string $accountName, string $idField, string $nameField): ?Account |
||
200 | { |
||
201 | |||
202 | $accountId = (int)$accountId; |
||
203 | $accountName = (string)$accountName; |
||
204 | // both empty? hard exit. |
||
205 | if ($accountId < 1 && strlen($accountName) === 0) { |
||
206 | $validator->errors()->add($idField, trans('validation.filled', ['attribute' => $idField])); |
||
207 | |||
208 | return null; |
||
209 | } |
||
210 | // ID belongs to user and is asset account: |
||
211 | /** @var AccountRepositoryInterface $repository */ |
||
212 | $repository = app(AccountRepositoryInterface::class); |
||
213 | $repository->setUser(auth()->user()); |
||
214 | $set = $repository->getAccountsById([$accountId]); |
||
215 | if ($set->count() === 1) { |
||
216 | /** @var Account $first */ |
||
217 | $first = $set->first(); |
||
218 | if ($first->accountType->type !== AccountType::ASSET) { |
||
219 | $validator->errors()->add($idField, trans('validation.belongs_user')); |
||
220 | |||
221 | return null; |
||
222 | } |
||
223 | |||
224 | // we ignore the account name at this point. |
||
225 | return $first; |
||
226 | } |
||
227 | |||
228 | $account = $repository->findByNameNull($accountName, [AccountType::ASSET]); |
||
229 | if (null === $account) { |
||
230 | $validator->errors()->add($nameField, trans('validation.belongs_user')); |
||
231 | |||
232 | return null; |
||
233 | } |
||
234 | |||
235 | return $account; |
||
236 | } |
||
237 | |||
238 | /** |
||
239 | * Adds an error to the validator when there are no transactions in the array of data. |
||
240 | * |
||
241 | * @param Validator $validator |
||
242 | */ |
||
243 | protected function atLeastOneTransaction(Validator $validator): void |
||
244 | { |
||
245 | $data = $validator->getData(); |
||
246 | $transactions = $data['transactions'] ?? []; |
||
247 | // need at least one transaction |
||
248 | if (count($transactions) === 0) { |
||
249 | $validator->errors()->add('description', trans('validation.at_least_one_transaction')); |
||
250 | } |
||
251 | } |
||
252 | |||
253 | /** |
||
254 | * Adds an error to the "description" field when the user has submitted no descriptions and no |
||
255 | * journal description. |
||
256 | * |
||
257 | * @param Validator $validator |
||
258 | */ |
||
259 | protected function checkValidDescriptions(Validator $validator) |
||
274 | } |
||
275 | |||
276 | } |
||
277 | |||
278 | /** |
||
279 | * Adds an error to the validator when the user submits a split transaction (more than 1 transactions) |
||
280 | * but does not give them a description. |
||
281 | * |
||
282 | * @param Validator $validator |
||
283 | */ |
||
284 | protected function emptySplitDescriptions(Validator $validator): void |
||
285 | { |
||
286 | $data = $validator->getData(); |
||
287 | $transactions = $data['transactions'] ?? []; |
||
288 | foreach ($transactions as $index => $transaction) { |
||
289 | $description = (string)($transaction['description'] ?? ''); |
||
290 | // filled description is mandatory for split transactions. |
||
291 | if (count($transactions) > 1 && strlen($description) === 0) { |
||
292 | $validator->errors()->add( |
||
293 | 'transactions.' . $index . '.description', |
||
294 | trans('validation.filled', ['attribute' => trans('validation.attributes.transaction_description')]) |
||
295 | ); |
||
296 | } |
||
297 | } |
||
298 | } |
||
299 | |||
300 | /** |
||
301 | * Adds an error to the validator when any transaction descriptions are equal to the journal description. |
||
302 | * |
||
303 | * @param Validator $validator |
||
304 | */ |
||
305 | protected function equalToJournalDescription(Validator $validator): void |
||
306 | { |
||
307 | $data = $validator->getData(); |
||
308 | $transactions = $data['transactions'] ?? []; |
||
309 | $journalDescription = (string)($data['description'] ?? ''); |
||
310 | foreach ($transactions as $index => $transaction) { |
||
311 | $description = (string)($transaction['description'] ?? ''); |
||
312 | // description cannot be equal to journal description. |
||
313 | if ($description === $journalDescription) { |
||
314 | $validator->errors()->add('transactions.' . $index . '.description', trans('validation.equal_description')); |
||
315 | } |
||
316 | } |
||
317 | } |
||
318 | |||
319 | /** |
||
320 | * If the transactions contain foreign amounts, there must also be foreign currency information. |
||
321 | * |
||
322 | * @param Validator $validator |
||
323 | */ |
||
324 | protected function foreignCurrencyInformation(Validator $validator): void |
||
325 | { |
||
326 | $data = $validator->getData(); |
||
327 | $transactions = $data['transactions'] ?? []; |
||
328 | foreach ($transactions as $index => $transaction) { |
||
329 | // must have currency info. |
||
330 | if (isset($transaction['foreign_amount']) |
||
331 | && !(isset($transaction['foreign_currency_id']) |
||
332 | || isset($transaction['foreign_currency_code']))) { |
||
333 | $validator->errors()->add( |
||
334 | 'transactions.' . $index . '.foreign_amount', |
||
335 | trans('validation.require_currency_info') |
||
336 | ); |
||
337 | } |
||
338 | } |
||
339 | } |
||
340 | |||
341 | /** |
||
342 | * Throws an error when the given opposing account (of type $type) is invalid. |
||
343 | * Empty data is allowed, system will default to cash. |
||
344 | * |
||
345 | * @param Validator $validator |
||
346 | * @param string $type |
||
347 | * @param int|null $accountId |
||
348 | * @param null|string $accountName |
||
349 | * @param string $idField |
||
350 | * |
||
351 | * @return null|Account |
||
352 | */ |
||
353 | protected function opposingAccountExists(Validator $validator, string $type, ?int $accountId, ?string $accountName, string $idField): ?Account |
||
354 | { |
||
355 | $accountId = (int)$accountId; |
||
356 | $accountName = (string)$accountName; |
||
357 | // both empty? done! |
||
358 | if ($accountId < 1 && strlen($accountName) === 0) { |
||
359 | return null; |
||
360 | } |
||
361 | if ($accountId !== 0) { |
||
362 | // ID belongs to user and is $type account: |
||
363 | /** @var AccountRepositoryInterface $repository */ |
||
364 | $repository = app(AccountRepositoryInterface::class); |
||
365 | $repository->setUser(auth()->user()); |
||
366 | $set = $repository->getAccountsById([$accountId]); |
||
367 | if ($set->count() === 1) { |
||
368 | /** @var Account $first */ |
||
369 | $first = $set->first(); |
||
370 | if ($first->accountType->type !== $type) { |
||
371 | $validator->errors()->add($idField, trans('validation.belongs_user')); |
||
372 | |||
373 | return null; |
||
374 | } |
||
375 | |||
376 | // we ignore the account name at this point. |
||
377 | return $first; |
||
378 | } |
||
379 | } |
||
380 | |||
381 | // not having an opposing account by this name is NOT a problem. |
||
382 | return null; |
||
383 | } |
||
384 | |||
385 | /** |
||
386 | * Validates the given account information. Switches on given transaction type. |
||
387 | * |
||
388 | * @param Validator $validator |
||
389 | * |
||
390 | * @throws FireflyException |
||
391 | */ |
||
392 | protected function validateAccountInformation(Validator $validator): void |
||
393 | { |
||
394 | $data = $validator->getData(); |
||
395 | $transactions = $data['transactions'] ?? []; |
||
396 | if (!isset($data['type'])) { |
||
397 | // the journal may exist in the request: |
||
398 | /** @var Transaction $transaction */ |
||
399 | $transaction = $this->route()->parameter('transaction'); |
||
400 | if (null === $transaction) { |
||
401 | return; // @codeCoverageIgnore |
||
402 | } |
||
403 | $data['type'] = strtolower($transaction->transactionJournal->transactionType->type); |
||
404 | } |
||
405 | foreach ($transactions as $index => $transaction) { |
||
406 | $sourceId = isset($transaction['source_id']) ? (int)$transaction['source_id'] : null; |
||
407 | $sourceName = $transaction['source_name'] ?? null; |
||
408 | $destinationId = isset($transaction['destination_id']) ? (int)$transaction['destination_id'] : null; |
||
409 | $destinationName = $transaction['destination_name'] ?? null; |
||
410 | $sourceAccount = null; |
||
411 | $destinationAccount = null; |
||
412 | switch ($data['type']) { |
||
413 | case 'withdrawal': |
||
414 | $idField = 'transactions.' . $index . '.source_id'; |
||
415 | $nameField = 'transactions.' . $index . '.source_name'; |
||
416 | $sourceAccount = $this->assetAccountExists($validator, $sourceId, $sourceName, $idField, $nameField); |
||
417 | $idField = 'transactions.' . $index . '.destination_id'; |
||
418 | $destinationAccount = $this->opposingAccountExists($validator, AccountType::EXPENSE, $destinationId, $destinationName, $idField); |
||
419 | break; |
||
420 | case 'deposit': |
||
421 | $idField = 'transactions.' . $index . '.source_id'; |
||
422 | $sourceAccount = $this->opposingAccountExists($validator, AccountType::REVENUE, $sourceId, $sourceName, $idField); |
||
423 | |||
424 | $idField = 'transactions.' . $index . '.destination_id'; |
||
425 | $nameField = 'transactions.' . $index . '.destination_name'; |
||
426 | $destinationAccount = $this->assetAccountExists($validator, $destinationId, $destinationName, $idField, $nameField); |
||
427 | break; |
||
428 | case 'transfer': |
||
429 | $idField = 'transactions.' . $index . '.source_id'; |
||
430 | $nameField = 'transactions.' . $index . '.source_name'; |
||
431 | $sourceAccount = $this->assetAccountExists($validator, $sourceId, $sourceName, $idField, $nameField); |
||
432 | |||
433 | $idField = 'transactions.' . $index . '.destination_id'; |
||
434 | $nameField = 'transactions.' . $index . '.destination_name'; |
||
435 | $destinationAccount = $this->assetAccountExists($validator, $destinationId, $destinationName, $idField, $nameField); |
||
436 | break; |
||
437 | default: |
||
438 | // @codeCoverageIgnoreStart |
||
439 | throw new FireflyException( |
||
440 | sprintf('The validator cannot handle transaction type "%s" in validateAccountInformation().', $data['type']) |
||
441 | ); |
||
442 | // @codeCoverageIgnoreEnd |
||
443 | |||
444 | } |
||
445 | // add some errors in case of same account submitted: |
||
446 | if (null !== $sourceAccount && null !== $destinationAccount && $sourceAccount->id === $destinationAccount->id) { |
||
447 | $validator->errors()->add($idField, trans('validation.source_equals_destination')); |
||
448 | } |
||
449 | } |
||
450 | } |
||
451 | |||
452 | /** |
||
453 | * @param Validator $validator |
||
454 | * |
||
455 | * @throws FireflyException |
||
456 | */ |
||
457 | protected function validateSplitAccounts(Validator $validator) |
||
509 | ); |
||
510 | // @codeCoverageIgnoreEnd |
||
511 | } |
||
512 | } |
||
513 | |||
514 | } |
||
515 |
As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next
break
.There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.
To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.