Odyssey
LoanDataGenerator.php
1 <?php
2 /**
3  * @copyright HomeCu 05/2019
4  *
5  * Generates sample loans with random for local testing. Run via command line in PHP.
6  *
7  * Usage:
8  * $obj = new LoanDataGenerator(CsvObject $csv_obj, DbLoanCreator $db_obj, LoanGeneratorMapper $mapper);
9  * $obj->execute();
10  *
11  * You will be prompted only with "how many loans to create" and asked to confirm.
12  * When complete, it will give you the test user passwords to log in with
13  * (see DB lnappuser for login info.)
14  *
15  * Change the random data by modifying sample-loans-05-19.csv.
16  */
18 {
19 
20  /** @var object Mapper object, which does all the mapping of CSV fields to proper DB fields */
21  protected $Mapper;
22 
23  /** @var object, the CsvToArray object which gives us the array to insert */
24  protected $CsvToArray;
25 
26  /** @var object, DB object which will write to the DB once we map it. */
27  protected $DbLoanCreator;
28 
29  /** @var string Use for passwords, confidence words, challenge questions in testing. */
30  protected $test_credentials = '1234';
31 
32  /** @var bool user has confirmed their selection of how many records to write */
33  protected $confirmed = false;
34 
35  /** @var int number of records user has entered */
36  protected $num_records = 0;
37 
38  /** @var int keep track of how many CSV lines we have read and bail if it reaches num_records */
39  protected $csv_line_count = 0;
40 
41  /** @var string user input line */
42  protected $line = '';
43 
44  /** @var int $schema_template - this is our template to duplicate for each loan, does not change (at this point) */
45  protected $schema_template = 8;
46 
47  /** @var array The detail template used for each loan's schema detail - we swap out loanid and unset detailid */
48  protected $detail_template = [];
49 
50  /** @var array data received from CsvToArray */
51  protected $csv_data = [];
52 
53  /** @var array where we store a running log of successful queries */
54  protected $success_counts = [];
55 
56  /** @var array ditto for errors. */
57  protected $errors = [];
58 
59  /**
60  * LoanDataGenerator constructor.
61  * @param CsvToArray $csv_obj instance
62  * @param DbLoanCreator $db_obj instance
63  * @param LoanGeneratorMapper $mapper instance
64  * @return void
65  */
66  public function __construct(CsvToArray $csv_obj, DbLoanCreator $db_obj, LoanGeneratorMapper $mapper) {
67 
68  $this
69  ->Set('Mapper', $mapper)
70  ->Set('CsvToArray', $csv_obj)
71  ->Set('DbLoanCreator', $db_obj);
72  }
73 
74  /**
75  * Entry point. Wait for user input once started.
76  * @throws Exception
77  * @return string
78  */
79  public function WaitForInput() {
80 
81  while ($this->line !== 'q') {
82 
83  echo $this->SetConsoleMessage();
84  echo $this
85  ->SetInputLine()
86  ->ProcessInput();
87  }
88 
89  return '';
90  }
91 
92  /**
93  * A typical setter, sets internal properties (CsvToArray, DbLoanCreator, LoanGeneratorMapper)
94  * Usually setters are public, don't think we'll need it
95  * @param string prop property identifier
96  * @param mixed property to set
97  * @return $this
98  */
99  protected function Set($prop, $object) {
100 
101  $this->{$prop} = $object;
102 
103  return $this;
104  }
105 
106  /**
107  * Set the line entered from input, if there is one.
108  * @return $this
109  */
110  protected function SetInputLine() {
111 
112  $handle = fopen ("php://stdin","r");
113  $this->line = trim(fgets($handle));
114  fclose($handle);
115 
116  return $this;
117  }
118 
119  /**
120  * Set prompt messages. These prompts should come in this order.
121  * @throws Exception
122  * @return string
123  */
124  protected function SetConsoleMessage() {
125 
126  if ($this->UserHasConfirmed()) {
127  // Set loose the Kraken.
128  echo "One moment, generating loan data . . . " . PHP_EOL;
129  return $this->GenerateLoanRecords();
130  }
131 
132  if ($this->UserHasSelectedVolume()) {
133  return "
134  This program will now generate {$this->num_records} loan records.
135  Enter y to continue, n to cancel:" . PHP_EOL;
136  }
137 
138  if ($this->IsInputEmpty() || ! $this->UserHasSelectedVolume()) {
139  return "
140  Enter q to quit at any time.
141  Enter the number of loan records you would like to generate:" . PHP_EOL;
142  }
143 
144  return null;
145  }
146 
147  /**
148  * Process the command line input.
149  * @return string
150  */
151  protected function ProcessInput() {
152 
153  if ($this->line === 'q') {
154  return "Exiting program . . ." . PHP_EOL;
155  exit;
156  }
157 
158  if (! $this->UserHasSelectedVolume()) {
159  $this->num_records = $this->line;
160 
161 
162  } elseif (! $this->UserHasConfirmed()) {
163 
164  if ($this->line === 'n') {
165  $this->num_records = 0;
166  return '';
167  }
168 
169  $this->confirmed = $this->line === 'y';
170  }
171 
172  return '';
173  }
174 
175  /**
176  * Is $this->line property empty? (or zero)
177  * @return bool
178  */
179  protected function IsInputEmpty() {
180 
181  return empty($this->line);
182  }
183 
184  /**
185  * Has the user selected how many records to create?
186  * @return bool
187  */
188  protected function UserHasSelectedVolume() {
189 
190  return $this->num_records > 0;
191  }
192 
193  /**
194  * Has the user confirmed their choice?
195  * @return bool
196  */
197  protected function UserHasConfirmed() {
198 
199  return $this->confirmed === true;
200  }
201 
202  /**
203  * Call up the actual generator class and get busy.
204  * @throws Exception
205  * @return string
206  */
207  protected function GenerateLoanRecords() {
208 
209  return $this
210  ->ParseCsv()
211  ->SetDetailTemplate()
212  ->MapCsvToData()
213  ->ReturnResponse();
214  }
215 
216 
217  /**
218  * Use CsvToArray to parse the CSV file into an array.
219  * @return $this
220  */
221  protected function ParseCsv() {
222 
223  while ($this->ContinueProcessing()) {
224 
225  $this->csv_line_count++;
226 
227  $data = $this->CsvToArray->ParseData();
228 
229  if (isset($data['errors']) && (count($data['errors']) > 0)) {
230  // Mock a table handle to canonicalize error output.
231  $this->AddNewErrors('csv', $data['errors']);
232  return $this;
233  }
234 
235  $this->csv_data[] = $data['data'];
236  }
237 
238  return $this;
239  }
240 
241  /**
242  * If we have reached number of lines or end of file, stop processing;
243  * if exceeded num_records, close the CSV, handle will still be open.
244  * @return bool
245  */
246  protected function ContinueProcessing() {
247 
248  if ($this->csv_line_count > $this->num_records - 1) {
249  $this->CsvToArray->CloseFile();
250  return false;
251  }
252 
253  if (! $this->CsvToArray->IsLinesToProcess()) {
254  return false;
255  }
256 
257  return true;
258 
259  }
260 
261  /**
262  * Get detail record ID 8 to be applied to every loan we insert. All we will need
263  * to do is swap out loan id and unset detail id for each record, see
264  * Mapper->createDetailTemplateMap()
265  * @return $this
266  */
267  protected function SetDetailTemplate() {
268 
269  if ($this->IsErrors()) {
270  return $this;
271  }
272 
273  $data = $this->DbLoanCreator->getList('lnappschemadetail', 'loanid', $this->schema_template);
274 
275  $this->detail_template = $data['data'];
276 
277  return $this;
278  }
279 
280  /**
281  * For each line of the CSV, map the data to the tables, which is an unavoidable several steps.
282  * We must first get the user insert so we can have a user ID for the question records. Then
283  * we can move on to the loan, but there is no clear way to associate our CSV fields with
284  * loan data fields to create formname_[schema record id] for the JSON. This form key
285  * pattern is what makes the data load properly in the end user loan app.
286  * @throws Exception
287  * @return $this
288  */
289  protected function MapCsvToData() {
290 
291  if ($this->IsErrors()) {
292  return $this;
293  }
294 
295  $count = 0;
296 
297  foreach ($this->csv_data as $index => $row) {
298 
299  $count++;
300  $loan_id = $this->CreateSchemaRecords($row);
301  $this->InsertUser($row);
302  $user_id = $this->GetLoanAppUserId();
303 
304  $this
305  ->InsertUserLoan($loan_id, $user_id, $row)
306  ->InsertChallengeResponses($user_id);
307 
308  // Should already be done at continueProcessing, double guard.
309  if ($count >= $this->num_records) {
310  break;
311  }
312  }
313 
314  return $this;
315  }
316 
317  /**
318  * Set the master schema and get the id (see comments below.) Set the detail schema.
319  * @param array $row a row of data from the CSV.
320  * @throws Exception
321  * @return int
322  */
323  protected function CreateSchemaRecords($row) {
324 
325  $loan_id = $this->CreateMasterSchema($row);
326  $this->CreateDetailSchema($loan_id);
327 
328  return $loan_id;
329  }
330 
331  /**
332  * Create the master schema and return the ID of the inserted record. In later PHP/SQL
333  * versions, this will automatically return the record ID inserted, but can't do
334  * that here so we are doing two things in one method (which is bad.)
335  * @param array $row
336  * @throws Exception
337  * @return int
338  */
339  protected function CreateMasterSchema($row) {
340 
341  $map = $this->Mapper->MasterSchemaMap($row);
342  $response = $this->DbLoanCreator->AddSampleData($map);
343  $this->UpdateResponses($response);
344 
345  $data = $this->DbLoanCreator->GetLastRecord('lnappschemamaster', 'loanid');
346 
347  return $data['data']['loanid'];
348  }
349 
350  /**
351  * Insert the schema detail records for our current loan.
352  * @param int $loan_id
353  * @return $this
354  */
355  protected function CreateDetailSchema($loan_id) {
356 
357  $map = $this->Mapper->CreateDetailTemplateMap($loan_id, $this->detail_template);
358 
359  $response = $this->DbLoanCreator->AddSampleData($map);
360 
361  $this->UpdateResponses($response);
362 
363  return $this;
364  }
365 
366  /**
367  * Insert the loan data. (@TODO since we are now using native pg_methods, we should
368  * change our inserts to ' ... returning userid'
369  * so we don't need this exrta query)
370  * @param int $loan_id
371  * @param int $user_id
372  * @param array $row
373  * @throws Exception
374  * @return $this
375  */
376  protected function InsertUserLoan($loan_id, $user_id, $row) {
377 
378  // Create a json map [csv key] => [formfield_[this schema field id]]
379  $data = $this->DbLoanCreator->GetList('lnappschemadetail', 'loanid', $loan_id);
380  $map_user = $this->Mapper->MapUserLoan($user_id, $loan_id, $row, $data);
381  $response = $this->DbLoanCreator->AddSampleData($map_user);
382  $this->UpdateResponses($response);
383 
384  return $this;
385  }
386 
387  /**
388  * Insert three random challenge responses so we can test login process.
389  * @param int $user_id
390  * @throws Exception
391  * @return $this
392  */
393  protected function InsertChallengeResponses($user_id) {
394 
395  $rand_resp = $this->Mapper->MapRandomQuestionResponses($user_id, $this->test_credentials);
396  $response = $this->DbLoanCreator->AddSampleData($rand_resp);
397  $this->UpdateResponses($response);
398 
399  return $this;
400  }
401 
402  /**
403  * Insert the user data for the loan. Do first so we can get a user id.
404  * (@TODO since we are now using native pg_methods, we should change
405  * our inserts to ' ... returning userid'
406  * @param $row
407  * @return $this
408  */
409  protected function InsertUser($row) {
410 
411  $user_map = $this->Mapper->MapUserData($row, $this->test_credentials);
412  $response = $this->DbLoanCreator->AddSampleData($user_map);
413 
414  $this->UpdateResponses($response);
415 
416  return $this;
417  }
418 
419  /**
420  * Get the user id from the record we just inserted (@TODO since we are now using
421  * native pg_methods, we should change our inserts to ' ... returning userid'
422  * so we don't need this exrta query)
423  * @return int
424  */
425  protected function GetLoanAppUserId() {
426 
427  $data = $this->DbLoanCreator->GetLastRecord('lnappuser', 'userid');
428 
429  if (isset($data['data']) && isset($data['data']['userid'])) {
430  return $data['data']['userid'];
431  }
432 
433  return 0;
434  }
435 
436  /**
437  * Compose a response that should be everything that went right. Or wrong.
438  * @return string
439  */
440  protected function ReturnResponse() {
441 
442  $plural = ($this->num_records == 1)? 'record' : 'records';
443 
444  $msg = "
445  Done creating {$this->num_records} loan $plural." .
446  PHP_EOL .
447  "
448  To test users, select returning non member at the loan app
449  screen and log in. All passwords and security questions are
450  answered with '{$this->test_credentials}.'" .
451  PHP_EOL .
452  $this->SuccessResponses() .
453  $this->ErrorResponses() .
454  PHP_EOL .
455  "
456  When you are done testing, run 'php cleanup-test-records.php'
457  to remove all test records. Type 'q' to exit." .
458  PHP_EOL;
459 
460  $this->ResetInputValues();
461 
462  return $msg;
463  }
464 
465  /**
466  * Helper for returnResponse(). walk the successful insert counts and create
467  * sensible message strings of them.
468  * @return string|null
469  */
470  protected function SuccessResponses() {
471 
472  $msg = null;
473  if (count($this->success_counts) > 0) {
474  foreach ($this->success_counts as $table => $count) {
475 
476  $plural = ($count == 1)? 'record' : 'records';
477  $msg .= "$count $plural inserted for table $table." . PHP_EOL;
478  }
479  }
480 
481  return $msg;
482  }
483 
484  /**
485  * Ditto, helper for ReturnResponse().
486  * @return string|null
487  */
488  protected function ErrorResponses() {
489 
490  $msg = null;
491 
492  if ($this->IsErrors()) {
493  foreach ($this->errors as $table => $error_arr) {
494  foreach ($error_arr as $str) {
495 
496  $msg .= "Error for table $table : $str" . PHP_EOL;
497  }
498  }
499  }
500 
501  return $msg;
502  }
503 
504  /**
505  * When done, flush these out to return to the prompt screen.
506  * @return $this
507  */
508  protected function ResetInputValues() {
509 
510  $this->num_records = 0;
511  $this->confirmed = false;
512 
513  return $this;
514  }
515 
516  /**
517  * Update the error array and success counts.
518  * @param array $response
519  * @return $this
520  */
521  protected function UpdateResponses($response = []) {
522 
523  if (count($response) == 0) {
524  return $this;
525  }
526 
527  $this
528  ->UpdateSuccessCounts($response)
529  ->UpdateResponseErrors($response);
530 
531  return $this;
532  }
533 
534  /**
535  * Update the success counts, to be used in a plain-English message in ReturnResponse().
536  * @param array $response
537  * @return $this
538  */
539  protected function UpdateSuccessCounts($response = []) {
540 
541  if ((isset($response['success']) && count($response['success']) > 0)) {
542  foreach ($response['success'] as $table => $count) {
543 
544  if (isset($this->success_counts[$table])) {
545  $this->success_counts[$table] += $response['success'][$table];
546  continue;
547  }
548 
549  $this->success_counts[$table] = $response['success'][$table];
550  }
551  }
552 
553  return $this;
554  }
555 
556  /**
557  * Set any response errors in our response array. Walking through and adding
558  * in the event other errors are added before or after.
559  * @param array $response
560  * @return $this
561  */
562  protected function UpdateResponseErrors($response = []) {
563 
564  if ((isset($response['errors']) && count($response['errors']) > 0)) {
565  foreach ($response['errors'] as $table => $error_arr) {
566 
567  $this->AddNewErrors($table, $error_arr);
568  }
569  }
570 
571  return $this;
572  }
573 
574  /**
575  * Helper for above to simplify code. Don't have to test values, wouldn't
576  * be here is there wasn't at least one member in $error_arr
577  * @param string $table
578  * @param array $error_arr
579  * @return $this
580  */
581  protected function AddNewErrors($table, $error_arr) {
582 
583  if (! isset($this->errors[$table])) {
584  $this->errors[$table] = [];
585  }
586 
587  foreach ($error_arr as $err) {
588  $this->errors[$table][] = $err;
589  }
590 
591  return $this;
592  }
593 
594  /**
595  * Got errors?
596  * @return bool
597  */
598  protected function IsErrors() {
599 
600  return count($this->errors) > 0;
601  }
602 
603 }
AddSampleData($data=[])
UpdateResponses($response=[])
GetLastRecord($table, $keyfield)
InsertChallengeResponses($user_id)
AddNewErrors($table, $error_arr)
Set($prop, $object)
__construct(CsvToArray $csv_obj, DbLoanCreator $db_obj, LoanGeneratorMapper $mapper)
UpdateSuccessCounts($response=[])
InsertUserLoan($loan_id, $user_id, $row)
UpdateResponseErrors($response=[])
IsLinesToProcess()
Definition: CsvToArray.php:91
GetList($table, $keyfield, $value)