xref: /web-bugs/src/Utils/Captcha.php (revision 3f4ad27d)
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