mobicms/captcha
Mobicms Captcha is a lightweight CAPTCHA package for Laravel that generates customizable image challenges to protect forms from bots. It offers a simple API for rendering and validating codes, with configurable options for easy integration into login, registration, and contact forms.
Install the package with Composer:
composer require mobicms/captcha
Because this is a framework-agnostic PHP package, Laravel will not auto-register a service provider. Start by reading the package source in vendor/mobicms/captcha/ and its README to confirm the exact class names and method signatures. The typical lifecycle is:
A minimal first use case is protecting a contact or login form:
// routes/web.php
Route::get('/captcha', [CaptchaController::class, 'image'])->name('captcha.image');
Route::post('/contact', [ContactController::class, 'store']);
// app/Http/Controllers/CaptchaController.php
public function image()
{
// Inspect the package API for the exact generator call.
// The goal is to obtain the generated code and the image output.
$captcha = new \Mobicms\Captcha\Captcha();
// Store the expected answer using Laravel's session, not $_SESSION.
session(['captcha_answer' => $captcha->getCode()]);
return response($captcha->getImage())
->header('Content-Type', 'image/png')
->header('Cache-Control', 'no-store, no-cache, must-revalidate');
}
In the form:
<form method="POST" action="/contact">
@csrf
<img src="{{ route('captcha.image') }}?t={{ now()->timestamp }}" alt="CAPTCHA">
<input type="text" name="captcha" autocomplete="off">
<button type="submit">Send</button>
</form>
Then validate on submission:
$request->validate([
'captcha' => ['required', function ($attribute, $value, $fail) {
$expected = session('captcha_answer');
session()->forget('captcha_answer');
if (! $expected || ! hash_equals(strtoupper($expected), strtoupper(trim($value)))) {
$fail('The CAPTCHA answer is incorrect.');
}
}],
]);
The first thing to verify is whether the package writes directly to $_SESSION or exposes the generated code separately. If it tightly couples generation and validation through native PHP sessions, you will need to bridge it to Laravel’s session store or use only the image/code generation portion.
The cleanest Laravel integration is to use the package only to generate the image and the expected code, then manage the answer yourself in Laravel’s session. This avoids conflicts between native PHP sessions and Laravel’s session driver.
Pattern:
GET /captcha generates a new code.captcha.answer.Content-Type: image/png and Cache-Control: no-store.Encapsulate validation so it can be reused across forms:
class CaptchaRule implements ValidationRule
{
public function validate(string $attribute, mixed $value, Closure $fail): void
{
$expected = session()->pull('captcha.answer');
if (! $expected || ! hash_equals(strtoupper($expected), strtoupper(trim((string) $value)))) {
$fail('The CAPTCHA answer is incorrect.');
}
}
}
Usage:
$request->validate([
'captcha' => ['required', new CaptchaRule()],
]);
Add a cache-busting query string to the image URL so the browser requests a new challenge:
<img src="{{ route('captcha.image') }}?t={{ now()->timestamp }}" alt="CAPTCHA">
For an explicit refresh button, update the src with JavaScript:
<button type="button" onclick="document.getElementById('captcha-img').src = '{{ route('captcha.image') }}?t=' + Date.now()">
Refresh
</button>
Each request to the image route overwrites the session answer. That is expected: only the most recently displayed CAPTCHA should validate.
The session-based flow still works as long as the CAPTCHA image route and the form submission both use Laravel’s web middleware group and therefore share the same session.
For Livewire, prevent the image from being re-rendered unnecessarily:
<div wire:ignore>
<img id="captcha-img" src="{{ route('captcha.image') }}?t={{ now()->timestamp }}" alt="CAPTCHA">
</div>
For Inertia or SPA forms, render the <img> tag pointing to the Laravel route. The image request must include credentials/cookies so the session is preserved.
If the package allows configuration of length, fonts, colors, or image size, wrap instantiation in a small service or bind it in a service provider:
// app/Providers/AppServiceProvider.php
$this->app->bind(Captcha::class, function () {
return new Captcha([
'length' => 5,
// other package-specific options
]);
});
Then inject Captcha into controllers instead of constructing it inline. This makes testing and future package replacement easier.
Do not generate real CAPTCHA images in most feature tests. Either:
Example:
$this->withSession(['captcha.answer' => 'ABCDE'])
->post('/contact', [
'captcha' => 'ABCDE',
// other fields
]);
For unit tests around your validation rule, test the session comparison logic directly without invoking the image generator.
CAPTCHA generation can be abused to consume CPU or fill sessions. Apply Laravel’s rate limiter to the image route:
Route::get('/captcha', [CaptchaController::class, 'image'])
->middleware('throttle:30,1')
->name('captcha.image');
Also consider throttling form submissions independently.
$_SESSION vs Laravel sessionThis is the most important integration risk. mobicms/captcha is a standalone PHP package and may use PHP’s native $_SESSION or expect you to manage session state yourself. Laravel uses its own session abstraction. Do not assume $_SESSION['captcha'] is the same as session('captcha').
Recommended approach: inspect the package API. If it exposes the generated code separately from the image, use that code and store it with Laravel’s session() helper. If it only validates through native session state, either:
Avoid mixing Laravel sessions and native PHP sessions in the same request unless you fully understand the interaction.
The CAPTCHA image route and the form submission route must share the same session. In Laravel, that usually means both routes belong to the web middleware group. API routes often use the api group and do not have session support by default. A stateless API cannot use session-based CAPTCHA without adding cookie/session middleware or switching to a token-based approach.
Generated CAPTCHA codes are often uppercase or mixed case. Normalize both sides before comparing:
hash_equals(
strtoupper(trim($expected)),
strtoupper(trim($submitted))
);
Use trim() to avoid failures caused by accidental spaces. Use hash_equals() for a timing-safe comparison.
Always remove the session answer after a validation attempt, even if it fails:
$expected = session()->pull('captcha.answer');
If you leave it in session, the same challenge can be reused for multiple submissions, weakening spam protection. If the validation fails, require the user to load a new CAPTCHA image.
If the CAPTCHA image is cached by the browser, users may see the same challenge even after a refresh. Always send no-cache headers and add a cache-busting query string:
return response($image)
->header('Content-Type', 'image/png')
->header('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
->header('Pragma', 'no-cache');
Some lightweight CAPTCHA packages output the image directly instead of returning a string or resource. If so, capture it:
ob_start();
$captcha->render(); // or whatever the package method is
$image = ob_get_clean();
return response($image)->header('Content-Type', 'image/png');
Check the package source for whether it calls header(), echo, or returns a value. If it sends headers directly, you may need to adapt or fork the rendering path.
Most image-based CAPTCHA packages require the PHP GD extension. If the image route returns a broken image or a 500 error, check that ext-gd is installed and enabled. For containerized or serverless deployments, this is a common missing dependency.
Image CAPTCHAs are inaccessible to screen-reader users. For production forms, consider offering an audio challenge or a simple non-image alternative, or use a different anti-spam mechanism such as honeypots or time-based checks. If accessibility is a hard requirement, this package may not be sufficient on its own.
Under Laravel Octane, long-lived workers can expose static state bugs. Verify that the package does not store per-request data in static properties or global variables. If it does, you may need to instantiate it per request and avoid caching the instance across requests.
If the package supports custom fonts, colors, image dimensions, or code length, centralize those settings in a config file or service provider binding rather than scattering constructor arguments across controllers. This also makes it easier to swap the CAPTCHA implementation later.
If validation always fails:
$_SESSION instead of Laravel’s session.How can I help you explore Laravel packages today?