```
<?php
declare(strict_types=1);

namespace app\components;

// PHP built-in exception:
use InvalidArgumentException;

use Yii;
use yii\base\Model;
use yii\web\Request;

use app\components\traits\CacheJsCssTrait;
use app\helpers\LogHelper;
use app\helpers\ParamHelper;
use app\helpers\TimeHelper;

/**
* Honeypot for Controller class 
*
* Controller usage example:
* 
* 	public function actionContact()
* 	{
* 		$model = new \app\models\ContactForm();
* 		
*		// (Optional) You can define customized pools per model if you want,
*		// otherwise the manager falls back to default pool
* 		$honeypotPools = [
* 			\app\models\ContactForm::class => ['firstname', 'lastname', 'username', 'email', 'phone', 
*				'address', 'company', 'website', 'city', 'zip', 'comment'],
*			
* 			// You can add others models here
* 		];
* 		
*		// Instantiate your HoneypotManager passing your customized pools, min, max traps
* 		$honeypotManager = new \app\components\HoneypotManager($honeypotPools, 2, 5);
*
*		// Prepare traps: select traps to display and fatal traps randomly
* 		$hpData = $honeypotManager->prepareHoneypots($model);
*		$hpFields = $hpData['displayed'];
*		$hpFatalFields = $hpData['fatal'];
* 		
* 		$request = \Yii::$app->request;
*		
*		// Load POST data, log filled honeypots, and validate the model; returns true if valid
*		if ($honeypotManager->handleRequest($model, $hpFields))
* 		{
* 			if ($model->process())
* 			{
* 				\Yii::$app->session->setFlash('success', 'Thanks for contacting us.');
* 				return $this->refresh();
* 			}
*			
*			// Check if any fatal honeypot trap was triggered
*			if ($model->isFatalHoneypotFilled())
*			{
*				 // Example: mark the model’s status as spam and save without further validation
*				$model->status = 'spam';
*				$model->save(false);
*			}
* 			
* 			\Yii::$app->session->setFlash('error', 'Failed to process form.');
* 		}
* 		
* 		return $this->render('contact', [
* 			'model' => $model,
*			// Honeypot variables below:
* 			'hpFields' => $hpFields,// Honeypot - For the view: which fields to render hidden
* 			'hpFatalFields' => $hpFatalFields,// Honeypot - For logging or debugging. Optional
* 		]);
* 	}
*
* View useage example:
* 
* 	$css = <<<CSS
* 	.hp-hidden,
* 	.hp-hidden input,
* 	.hp-hidden textarea
* 	{
* 		position: absolute !important;
* 		left: -9999px !important;
* 		top: -9999px !important;
* 		height: 1px !important;
* 		width: 1px !important;
* 		overflow: hidden !important;
* 		pointer-events: none !important;
* 	}
* 	CSS;
* 	
* 	$this->registerCss($css);
* 	?>
* 	
* 	<?php
* 	// Render honeypot fields hidden
*	$placeholders = $model::honeypotPlaceholders();
*	
* 	foreach ($hpFields as $field) :
*		$placeholder = $placeholders[$field] ?? "Enter " . ucfirst($field);
*		$label = $labels[$field] ?? ucfirst($field);
* 	?>
* 	<?= $form->field($model, $field, ['options' => ['class' => 'hp-hidden']])
* 		->textInput([
*			'tabindex' => '-1',
*			'autocomplete' => 'off',
*			'placeholder' => $placeholder,
*			'class' => 'form-control square'
* 		])->label($label);
* 	?>
* 	<?php endforeach; ?>
* 	
* 	<?php
* 	// For debugging only, includes fatal honeypot names in HTML comments
* 	if (!empty($hpFatalFields)) : 
* 	?>
* 	<?= implode(', ', $hpFatalFields); ?>
* 	<?php endif; ?>
*/
class HoneypotManager
{
	private bool $_debugMode = true;
	
	/**
	* @var array mapping model class names => pool of honeypot fields
	* If model class not listed here, fallback to defaultPool
	*/
	protected array $__modelPools = [];
	
	/**
	* @var string[] Default honeypot trap names used if no model-specific pool found
	*/
	protected array $__defaultPool = ['firstname', 'lastname', 'username', 'email', 'phone', 'address', 'company', 'website', 'city', 'zip', 'comment'];
	
	protected int $__minCount;
	protected int $__maxCount;
	
	// Note: Every protected variable below CacheJsCssTrait is used by the trait
	use CacheJsCssTrait;
	
	protected bool $__cacheDebugMode = false;
	protected bool $__compressJsCss = false;
	protected bool $__cacheEnable = false;
	protected int $__cacheJsCssTime = 3600;// +1 hour
	protected string $__cacheKeyPrefix = 'honeypot_manager_css';
	
	const DEFAULT_DURATION_SECONDS = 3600;// +1 hour
	
	/**
	* Construct
	* @param array $modelPools Optional per-model trap pools
	* @param int $minCount min traps per form
	* @param int $maxCount max traps per form
	* @param array|null $defaultPool Global default pool
	*/
	public function __construct(
		array $modelPools = [],
		int $minCount = 2,
		int $maxCount = 7,
		?array $defaultPool = null
	)
	{
		$this->__modelPools = $modelPools;
		
		$this->__minCount = $minCount;
		$this->__maxCount = $maxCount;
		$this->__defaultPool = !empty($defaultPool) ? $defaultPool 
			: (!empty($modelPools) ? $modelPools : $this->__defaultPool);
		
		// Get cacheDuration from Param and set with TimeHelper
		$cacheDuration = ParamHelper::get('widgets.cacheDuration');
		$this->__cacheJsCssTime = TimeHelper::convertDurationToSeconds($cacheDuration);
		
		// Cache & Compression
		$this->__cacheEnable = ParamHelper::get('widgets.cacheEnable');
		$this->__compressJsCss = ParamHelper::get('widgets.compressJsCss');
	}
	
	/**
	* Prepares honeypots for the given model.
	* @param Model $model Model using HoneypotTrait with setHoneypots()/setFatalHoneypots()
	* @return array ['displayed' => array, 'fatal' => array]
	* @throws \InvalidArgumentException
	*/
	public function prepareHoneypots(Model $model): array
	{
		if (!method_exists($model, 'setHoneypots')
			|| !method_exists($model, 'setFatalHoneypots')
		)
		{
			throw new InvalidArgumentException(sprintf(
				'Model %s must implement setHoneypots() and setFatalHoneypots()',
				get_class($model)
			));
		}
		
		$modelClass = get_class($model);
		
		$pool = $this->__modelPools[$modelClass]
			?? (method_exists($modelClass, 'honeypotPool') ? $modelClass::honeypotPool() 
			: $this->__defaultPool);
		
		$count = random_int($this->__minCount, min($this->__maxCount, count($pool)));
		
		$selected = (array) array_rand(array_flip($pool), $count);
		
		if (is_string($selected))
		{
			$selected = [$selected];
		}
		
		$fatalCount = random_int(1, (int) max(1, floor(count($selected) / 2)));
		$fatal = (array) array_rand(array_flip($selected), $fatalCount);
			
		if (is_string($fatal))
		{
			$fatal = [$fatal];
		}
		
		if ($this->_debugMode)
		{
			LogHelper::debug('Honeypot Manager - Prepare',
				[
					'model' => $modelClass,
					'pool' => $pool,
					'selected' => $selected,
					'fatal' => $fatal,
					'method' => __METHOD__
				]
			);
		}
		
		$model->setHoneypots($selected);
		$model->setFatalHoneypots($fatal);
		
		return ['displayed' => $selected, 'fatal' => $fatal];
	}
	
	/**
	* Handle Javascript and Caching
	*/
	protected function registerCss(): void
	{
		$cacheKey = $this->__cacheKeyPrefix . $widgetId . '_css_block_';
		
		$cssBlock = $this->getCachedCssBlock($cacheKey, function ()
		{
			return <<<CSS
			CSS;
		});
		
		// Register the JavaScript.
		$this->getView()->registerCss($cssBlock);
	}
	
	/**
	* Loads POST data into model, logs filled honeypots, and validates the model
	* @param Model $model Form model with HoneypotTrait
	* @param Request|null $request Optional request object, defaults to Yii::$app->request
	* @param array $honeypots The honeypot fields assigned (normally from prepareHoneypots)
	* @return bool True if load & validate succeeded, false otherwise
	*/
	public function handleRequest(Model $model, array $honeypots, ?Request $request = null): bool
	{
		$request = $request ?? \Yii::$app->request;
		
		if ($request->isPost && $model->load($request->post()))
		{
			$filled = [];
			
			foreach ($honeypots as $field)
			{
				$val = $model->$field ?? null;
				
				if ($val !== null && $val !== '')
				{
					$filled[$field] = $val;
				}
			}
			
			if ($this->_debugMode)
			{
				LogHelper::debug('Honeypot Manager - Handle request',
					[
						'filledHoneypots' => $filled,
						'ip' => $request->userIP,
						'userAgent' => $request->userAgent,
						'method' => __METHOD__
					]
				);
			}
			
			return $model->validate();
		}
		
		return false;
	}
}

<?php
declare(strict_types=1);

namespace app\components\traits;

use yii\base\Model;

use app\helpers\LogHelper;
use app\helpers\RoleHelper;

/**
* Trait HoneypotTrait
* Provides reusable honeypot anti-spam protection for Yii2 Models.
*
* HOW TO USE:
*
* 1) Define in your Model:
*    - Public properties for your form fields *and* their honeypot variants.
*      Example:
*          public string $name = '';
*          public string $namee = ''; // honeypot variant for 'name'
*
*    - A property called $honeypotPairs, mapping logical field names to a pair of attributes:
*          [
*              'name' => ['name', 'namee'],
*              'email' => ['email', 'emaill'],
*              'subject' => ['subject', 'subjectt'],
*              // do NOT include large fields like 'body' if you want to exclude from honeypots
*          ]
*
* 2) Add HoneypotTrait to your Model class:
*      use \app\components\HoneypotTrait;
*
* 3) In your model's validation rules(), include:
*      - Required validation only for the *active* (non-honeypot) field.
*      - Call `validateHoneypotEmpty` on honeypot fields to ensure they remain empty.
*
* 4) In your controller action handling the form:
*      - Instantiate your model.
*      - Generate honeypot assignments by randomly selecting which version in each pair is the honeypot.
*      - Store the list of honeypot fields in the model using setHoneypots().
*      - Pass honeypot info to your view for rendering.
*      - On POST, load and validate model normally.
*      - Honeypot validation will trigger errors if bots fill the honeypot fields.
*
* 5) In your form view:
*      - Render all form fields *and* their honeypot counterparts.
*      - Visibly show only the *real* (active) fields.
*      - Hide honeypot fields with CSS (e.g., visually off-screen) so human users don’t see them.
*
* WHY THIS WORKS:
*
* - Bots that autofill all form inputs will fill the honeypot fields, triggering validation errors.
* - Real users never fill honeypots (because they are hidden).
* - Randomizing honeypot selection per request or session prevents bots from learning which fields to avoid.
* - Mapping logical fields to pairs lets your validator know which attributes to require vs which are honeypots.
*
* IMPORTANT NOTES:
*
* - Do NOT randomize the real form attribute *names* themselves — keep consistent names for correct Yii2 model loading.
* - Define honeypot variants that closely resemble real fields to avoid bot detection by unusual names.
* - Usually restrict honeypots to basic inputs like 'name', 'email', 'subject' — exclude large text areas like 'body'.
*
* How to use in model/form:
* 
* 	namespace app\models;
* 	
* 	use yii\base\Model;
* 	use app\components\HoneypotTrait;
* 	
* 	class ContactForm extends Model
* 	{
* 		use HoneypotTrait;
* 		
* 		public string $name = '';
* 		public string $email = '';
* 		public string $subject = '';
* 		public string $body = '';
* 		
* 		public function rules()
* 		{
* 			return array_merge([
* 				[['name', 'email', 'subject', 'body'], 'required'],
* 				['email', 'email'],
* 			], $this->honeypotRules());
* 		}
* 		
* 		// Placeholder for actual processing logic (sending email, saving...)
* 		public function process(): bool
* 		{
* 			return true;
* 		}
* 	}
*/
trait HoneypotTrait
{
	private bool $_debugMode = true;

	// Declare honeypot trap fields as public properties
	public string $firstname = '';
	public string $lastname = '';
	public string $username = '';
	public string $email = '';
	public string $phone = '';
	public string $address = '';
	public string $company = '';
	public string $website = '';
	public string $city = '';
	public string $zip = '';
	public string $comment = '';
	
	/**
	* Returns the full pool of honeypot field names.
	* Override to customize per model.
	*/
	public static function honeypotPool(): array
	{
		return ['firstname', 'lastname', 'username', 'email', 'phone', 'address', 'company', 'website', 'city', 'zip', 'comment'];
	}
	
	/**
	* Returns user-friendly placeholders matching the honeypot fields.
	* Override this method if you want to customize placeholders per model.
	*/
	public static function honeypotPlaceholders(): array
	{
		return [
			'firstname' => \Yii::t('app', 'Enter first name'),
			'lastname' => \Yii::t('app', 'Enter last name'),
			'username' => \Yii::t('app', 'Enter username'),
			'email' => \Yii::t('app', 'Enter email'),
			'phone' => \Yii::t('app', 'Enter phone number'),
			'address' => \Yii::t('app', 'Enter address'),
			'company' => \Yii::t('app', 'Enter company name'),
			'website' => \Yii::t('app', 'Enter website URL'),
			'city' => \Yii::t('app', 'Enter city'),
			'zip' => \Yii::t('app', 'Enter ZIP/postal code'),
			'comment' => \Yii::t('app', 'Enter comment'),
		];
	}
	
	/**
	* Array of currently assigned honeypot fields for this request/session.
	* @var string[]
	*/
	protected array $__honeypots = [];
	
	/**
	* Array of currently assigned *fatal* honeypot fields.
	* Filling one of these causes immediate validation failure.
	* @var string[]
	*/
	protected array $__fatalHoneypots = [];
	
	/**
	* Assigns the honeypot fields for the current request or user session.
	* @param string[] $fields Array of attribute names assigned as honeypots.
	*
	* Typically called by controller after randomizing honeypot selection.
	*/
	public function setHoneypots(array $fields): void
	{
		$this->__honeypots = $fields;
		
		if ($this->_debugMode)
		{
			LogHelper::debug('HoneypotTrait - Set honeypots', ['fields' => $fields]);
		}
	}
	
	/**
	* Returns the currently assigned honeypot fields.
	* @return string[]
	*/
	public function getHoneypots(): array
	{
		return $this->__honeypots;
	}
	
	/**
	* Assign fatal honeypot fields for this request/session.
	* @param string[] $fields Array of attribute names causing immediate validation failure if filled.
	*/
	public function setFatalHoneypots(array $fields): void
	{
		$this->__fatalHoneypots = $fields;
		
		if ($this->_debugMode)
		{
			LogHelper::debug('HoneypotTrait - Set fatal honeypots', ['fields' => $fields]);
		}
	}
	
	/**
	* Returns currently assigned fatal honeypot fields.
	* @return string[]
	*/
	public function getFatalHoneypots(): array
	{
		return $this->__fatalHoneypots;
	}
	
	/**
	* Checks if any of the fatal honeypot fields have been filled
	* @return bool True if any fatal honeypot field is filled, false otherwise.
	*
	* This method inspects the current state of the model and returns true
	* if any of the honeypot traps classified as "fatal" contain values.
	*/
	public function isFatalHoneypotFilled(): bool
	{
		foreach ($this->getFatalHoneypots() as $fatalField)
		{
			if (!empty($this->$fatalField))
			{
				return true;
			}
		}
		
		return false;
	}
	
	/**
	* Returns validation rules for honeypots: safe + custom validator
	*/
	public function honeypotRules(): array
	{
		$pool = static::honeypotPool();
		
		return [
			[$pool, 'safe'],
			[$pool, 'validateHoneypots'],
		];
	}
	
	/**
	* Validator: fail if any of the active honeypots has a value.
	* @param string $attribute
	* @param array|null $params
	*/
	public function validateHoneypots(string $attribute, $params): void
	{
		// not an active honeypot
		if (!in_array($attribute, $this->__honeypots, true))
		{
			return;
		}
		
		// honeypot not filled
		if (empty($this->$attribute))
		{
			return;
		}
		
		$userIp = RoleHelper::userIp();
		$userAgent = RoleHelper::userAgent();
		
		if (in_array($attribute, $this->__fatalHoneypots, true))
		{
			LogHelper::logBotAction("Fatal honeypot trap '{$attribute}' filled - bot detected", [
				'ip' => $userIp,
				'userAgent' => $userAgent,
				'attribute' => $attribute,
				'method' => __METHOD__
			]);
			
			// Add generic error to block submission silently
			//$this->addError('_honeypots', 'Unable to process your submission.');
		}
		else
		{
			LogHelper::logBotAction("Non-fatal honeypot trap '{$attribute}' filled - suspicious bot activity", [
				'ip' => $userIp,
				'userAgent' => $userAgent,
				'attribute' => $attribute,
				'method' => __METHOD__
			]);
		}
	}
	
	/**
	* Adds virtual attribute to store errors related to honeypots
	*/
	public function attributes()
	{
		return array_merge(parent::attributes(), ['_honeypots']);
	}
}
```
and the view code
```
<?php
/**
* Honeypot
* @see app\components\traits\HoneypotTrait.php
*
* CSS class to hide honeypot inputs outside of viewport
*/
$css = <<<CSS
.hp-hidden,
.hp-hidden input,
.hp-hidden textarea
{
	position: absolute !important;
	left: -9999px !important;
	top: -9999px !important;
	height: 1px !important;
	width: 1px !important;
	overflow: hidden !important;
	pointer-events: none !important;
}
CSS;

$this->registerCss($css);
?>

<?php
/**
* Honeypot
* @see app\components\HoneypotManager.php
* @see app\components\traits\HoneypotTrait.php
*
* Render honeypot fields hidden
*/
$placeholders = $model::honeypotPlaceholders();

foreach ($hpFields as $field) :
	$placeholder = $placeholders[$field] ?? "Enter " . ucfirst($field);
	$label = $labels[$field] ?? ucfirst($field);
?>
<div class="col-md-12 mb-2">
<?= $form->field($model, $field, ['options' => ['class' => 'hp-hidden']])
	->textInput([
		//'tabindex' => '-1',
		//'autocomplete' => 'off',
		'placeholder' => $placeholder,
		'class' => 'form-control square',
])->label($label);
?>
</div><!-- .col-md-12 .mb-2 -->

<?php endforeach; ?>

<?php
// For debugging only, includes fatal honeypot names in HTML comments
if (YII_ENV_DEV && !empty($hpFatalFields)) :
?>
<pre style="color:#888;font-size:11px;">Fatal honeypots: <?= implode(', ', $hpFatalFields) ?></pre>

<?php endif; ?>
```

I would ike the render the css either in the honeypot class or honeypot trait and than call to be in the view somehow.