PluginProbe
Easy Invoice – Invoice Generator, PDF Quotes & Payments / 2.4.1
Easy Invoice – Invoice Generator, PDF Quotes & Payments v2.4.1
2.4.0 2.4.1 2.3.8 2.3.7 2.3.6 2.3.5 2.3.4 2.3.3 2.3.2 2.3.1 2.2.0 2.1.21 2.1.20 2.1.19 2.1.18 2.1.0 2.1.1 2.1.10 2.1.11 2.1.12 2.1.13 2.1.14 2.1.15 2.1.16 2.1.2 All 57 releases
← All changes | includes/Services/InvoiceNumberService.php +54 -2 2.3.72.4.1 View file →
@@ -40,9 +40,9 @@
40 40 // can't leak it on a fatal PHP error.
41 41 $lock_acquired = $this->acquireNumberLock();
42 42 try {
43 43 // Get the next number to use
44 - $next_number = get_option('easy_invoice_next_invoice_number', 1);
44 + $next_number = $this->freshCounter();
45 45
46 46 // Find the next unique number
47 47 $unique_number = $this->findNextUniqueNumber($next_number, $prefix);
48 48
@@ -116,9 +116,9 @@
116 116 // lock so neither call site is a race window.
117 117 $lock_acquired = $this->acquireNumberLock();
118 118 try {
119 119 // Get the next number to use from the current settings
120 - $next_number = get_option('easy_invoice_next_invoice_number', 1);
120 + $next_number = $this->freshCounter();
121 121
122 122 // Find the next unique number
123 123 $unique_number = $this->findNextUniqueNumber($next_number, $prefix);
124 124
@@ -146,8 +146,60 @@
146 146 * Timeout is 3s — if the database is so contended that even this
147 147 * fails, blocking the user's create-invoice request longer is
148 148 * worse than the residual race risk.
149 149 */
150 + /**
151 + * Keep a number a form submitted, or hand out a fresh one.
152 + *
153 + * The builder pre-fills its number field with the next number without
154 + * taking it, so two people who opened "New" at the same time both post
155 + * the same number. Under the number lock: a number nobody uses yet is
156 + * kept, and if it is the counter's next value the counter moves past
157 + * it; a number already in use is replaced by a freshly generated one.
158 + *
159 + * @param string $requested The number the form sent, possibly empty.
160 + * @return string The number to save.
161 + */
162 + public function claimOrGenerate(string $requested): string {
163 + $requested = trim($requested);
164 + if ($requested === '') {
165 + return $this->generateUniqueNumber();
166 + }
167 + $lock_acquired = $this->acquireNumberLock();
168 + try {
169 + if ($this->numberExists($requested)) {
170 + return $this->generateUniqueNumber();
171 + }
172 + $prefix = get_option('easy_invoice_invoice_prefix', 'INV-');
173 + $next = $this->freshCounter();
174 + if ($requested === $prefix . str_pad((string) $next, 6, '0', STR_PAD_LEFT)) {
175 + update_option('easy_invoice_next_invoice_number', $next + 1);
176 + }
177 + return $requested;
178 + } finally {
179 + if ($lock_acquired) {
180 + $this->releaseNumberLock();
181 + }
182 + }
183 + }
184 +
185 + /**
186 + * The counter as the database holds it right now.
187 + *
188 + * Every request loads the options table into memory at boot, before
189 + * it queues for the number lock, so a plain get_option() inside the
190 + * lock returns whatever the counter was when *this* request started —
191 + * and twelve simultaneous saves all "uniquely" took the same number.
192 + * Drop the cached copy and read it again once the lock is held.
193 + *
194 + * @return int
195 + */
196 + private function freshCounter(): int {
197 + wp_cache_delete('easy_invoice_next_invoice_number', 'options');
198 + wp_cache_delete('alloptions', 'options');
199 + return (int) get_option('easy_invoice_next_invoice_number', 1);
200 + }
201 +
150 202 private function acquireNumberLock(): bool {
151 203 global $wpdb;
152 204 $result = $wpdb->get_var($wpdb->prepare(
153 205 'SELECT GET_LOCK(%s, %d)',