1<?php 2 3namespace App\Utils; 4 5/** 6 * Captcha utility class for providing a simple math question with additions or 7 * subtractions to prevent spam. 8 */ 9class Captcha 10{ 11 /** 12 * First operand. 13 * @var int 14 */ 15 private $first; 16 17 /** 18 * Last operand. 19 * @var int 20 */ 21 private $last; 22 23 /** 24 * Highest possible operands value for randomization at initialization. 25 */ 26 const MAX = 50; 27 28 /** 29 * Supported equation operations. Keys are operation symbols and values are 30 * class method names to execute. 31 */ 32 const OPERATIONS = [ 33 '+' => 'addition', 34 '-' => 'subtraction', 35 ]; 36 37 /** 38 * Current operation. 39 * @var string 40 */ 41 private $operation; 42 43 /** 44 * Class constructor where operands random values and operation are set. 45 */ 46 public function __construct() 47 { 48 $this->randomize(); 49 } 50 51 /** 52 * Set random operands values and operation. 53 */ 54 public function randomize(): void 55 { 56 $this->setFirst(rand(1, self::MAX)); 57 $this->setLast(rand(1, self::MAX)); 58 $this->setOperation(self::OPERATIONS[array_rand(self::OPERATIONS)]); 59 } 60 61 /** 62 * First operand number setter to override default random pick. Defined as a 63 * separate method for convenience when unit testing. 64 */ 65 public function setFirst(int $number): void 66 { 67 $this->first = $number; 68 } 69 70 /** 71 * Last operand number setter to override default random pick. Defined as a 72 * separate method for convenience when unit testing. 73 */ 74 public function setLast(int $number): void 75 { 76 $this->last = $number; 77 } 78 79 /** 80 * Set the operation. If provided operation is invalid it falls back to addition. 81 */ 82 public function setOperation(string $operation): void 83 { 84 $this->operation = in_array($operation, self::OPERATIONS) ? $operation : 'addition'; 85 } 86 87 /** 88 * Get current question equation string for displaying it to the user. 89 */ 90 public function getQuestion(): string 91 { 92 $this->sortOperands(); 93 94 $symbol = array_search($this->operation, self::OPERATIONS); 95 $symbol = $symbol === false ? '+' : $symbol; 96 97 return $this->first.' '.$symbol.' '.$this->last.' = ?'; 98 } 99 100 /** 101 * The correct current answer of the given equation question. 102 */ 103 public function getAnswer(): int 104 { 105 $this->sortOperands(); 106 107 return \call_user_func([Captcha::class, $this->operation], $this->first, $this->last); 108 } 109 110 /** 111 * When the current operation is subtraction, sort operands to have a bigger 112 * operand first. With this, negative results are omitted for simplicity and 113 * possible better user experience. 114 */ 115 private function sortOperands(): void 116 { 117 $first = $this->first; 118 $last = $this->last; 119 120 if ($this->operation === 'subtraction') { 121 $this->first = $first > $last ? $first : $last; 122 $this->last = $first > $last ? $last : $first; 123 } 124 } 125 126 /** 127 * Addition of two operands. 128 */ 129 private function addition(int $first, int $last): int 130 { 131 return $first + $last; 132 } 133 134 /** 135 * Subtraction of two operands. 136 */ 137 private function subtraction(int $first, int $last): int 138 { 139 return $first - $last; 140 } 141} 142