@@ -81,6 +81,11 @@ class Validate {
8181 */
8282 public const NOT_START_WITH = 'not_start_with ' ;
8383
84+ /**
85+ * @var string Set a rate limit
86+ */
87+ public const RATE_LIMIT = 'rate_limit ' ;
88+
8489 private DB $ _db ;
8590
8691 private ?string $ _message = null ;
@@ -112,6 +117,7 @@ private function __construct() {
112117 * @param array $items subset of inputs to be validated
113118 *
114119 * @return Validate New instance of Validate.
120+ * @throws Exception If provided configuration for a rule is invalid - not if a provided value is invalid!
115121 */
116122 public static function check (array $ source , array $ items = []): Validate {
117123 $ validator = new Validate ();
@@ -318,6 +324,51 @@ public static function check(array $source, array $items = []): Validate {
318324 break ;
319325 }
320326 break ;
327+
328+ case self ::RATE_LIMIT :
329+ if (is_array ($ rule_value ) && count ($ rule_value ) === 2 ) {
330+ // If array treat as [limit, seconds]
331+ [$ limit , $ seconds ] = $ rule_value ;
332+ } else if (is_int ($ rule_value )) {
333+ // If integer default seconds to 60
334+ [$ limit , $ seconds ] = [$ rule_value , 60 ];
335+ }
336+
337+ if (!isset ($ limit ) || !isset ($ seconds )) {
338+ throw new Exception ('Invalid rate limit configuration ' );
339+ }
340+
341+ $ key = "rate_limit_ {$ item }" ;
342+ $ session = $ _SESSION [$ key ];
343+ $ time = date ('U ' );
344+ $ limit_end = $ time + $ seconds ;
345+
346+ if (isset ($ session ) && is_array ($ session ) && count ($ session ) === 2 ) {
347+ [$ count , $ expires ] = $ session ;
348+ $ diff = $ expires - $ time ;
349+
350+ if (++$ count >= $ limit && $ diff > 0 ) {
351+ $ validator ->addError ([
352+ 'field ' => $ item ,
353+ 'rule ' => self ::RATE_LIMIT ,
354+ 'fallback ' => "$ item has reached the rate limit which expires in $ diff seconds. " ,
355+ 'meta ' => ['expires ' => $ diff ],
356+ ]);
357+ break ;
358+ }
359+
360+ if ($ diff <= 0 ) {
361+ // Reset
362+ $ _SESSION [$ key ] = [1 , $ limit_end ];
363+ break ;
364+ }
365+
366+ $ _SESSION [$ key ] = [$ count , $ expires ];
367+ } else {
368+ $ _SESSION [$ key ] = [1 , $ limit_end ];
369+ }
370+
371+ break ;
321372 }
322373 }
323374 }
@@ -379,7 +430,7 @@ public function errors(): array {
379430 // Loop all errors to convert and get their custom messages
380431 foreach ($ this ->_to_convert as $ error ) {
381432
382- $ message = $ this ->getMessage ($ error ['field ' ], $ error ['rule ' ], $ error ['fallback ' ]);
433+ $ message = $ this ->getMessage ($ error ['field ' ], $ error ['rule ' ], $ error ['fallback ' ], $ error [ ' meta ' ] );
383434
384435 // If there is no generic `message()` set or the translated message is not equal to generic message
385436 // we can continue without worrying about duplications
@@ -409,10 +460,11 @@ public function errors(): array {
409460 * @param string $field name of field to search for.
410461 * @param string $rule rule which check failed. should be from the constants defined above.
411462 * @param string $fallback fallback default message if custom message and generic message are not supplied.
463+ * @param ?array $meta optional meta to provide to message.
412464 *
413465 * @return string Message for this field and rule.
414466 */
415- private function getMessage (string $ field , string $ rule , string $ fallback ): string {
467+ private function getMessage (string $ field , string $ rule , string $ fallback, ? array $ meta = [] ): string {
416468
417469 // No custom messages defined for this field
418470 if (!isset ($ this ->_messages [$ field ])) {
@@ -436,6 +488,11 @@ private function getMessage(string $field, string $rule, string $fallback): stri
436488 return $ this ->_message ?? $ fallback ;
437489 }
438490
491+ // If the message is a callback function, provide it with meta
492+ if (is_callable ($ this ->_messages [$ field ][$ rule ])) {
493+ return $ this ->_messages [$ field ][$ rule ]($ meta );
494+ }
495+
439496 // Rule-specific custom message was supplied
440497 return $ this ->_messages [$ field ][$ rule ];
441498 }
0 commit comments