diff --git a/CHANGELOG.md b/CHANGELOG.md
index e55a996..05bafe7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,14 +7,40 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
+### Added
+
+- The components under the package prefix: ``,
+ `` and ``.
+- The `laranail.captcha` container alias for `CaptchaService`.
+- The `laranail_captcha` string validation rule, implicit like the bare one, reporting the rule's own
+ translated message on failure (`CaptchaServiceProvider::VALIDATION_RULE`).
+- The canonical `laranail/captcha` view namespace, registered by package-tools over the same paths
+ as `laranail-captcha`.
+- A live-registry naming test (`tests/Feature/NamingConventionTest.php`) built on package-tools'
+ `AssertsRegisteredNames`.
+
### Changed
+- The components render through `laranail/captcha::components.*`. `laranail-captcha::` still
+ resolves the same files.
+- Docs, the README and the install command lead with the scoped tags and rule.
+- Requires `laranail/package-tools ^0.1.3`.
- `laranail::captcha.install` now extends laranail/package-tools' `InstallCommand` and takes
laranail/console's display API and run lifecycle from its `InteractsWithConsoleServices` and
`InteractsWithConsoleWriter` traits instead of its `Command` base. Name, option, description,
listing visibility, output and exit codes are unchanged and pinned by a new contract test. The
command is bound in the container because the new base takes the `Package` in its constructor.
+### Deprecated
+
+- The bare ``, `` and `` tags. They render the
+ same components and raise one `E_USER_DEPRECATED` notice when a template using them compiles.
+- The bare `captcha` validation rule. It validates as before, implicit included, with one
+ `E_USER_DEPRECATED` notice per process.
+- The bare `captcha` container alias. It resolves the same `CaptchaService`; it cannot raise a notice.
+
+Each is removed no earlier than the next minor after 0.1.
+
## [0.1.0] - 2026-08-15
### Added
diff --git a/README.md b/README.md
index b943702..f823455 100644
--- a/README.md
+++ b/README.md
@@ -45,7 +45,7 @@ To switch provider, set `CAPTCHA_PROVIDER`, `CAPTCHA_SITE_KEY` and `CAPTCHA_SECR
@@ -54,7 +54,7 @@ To switch provider, set `CAPTCHA_PROVIDER`, `CAPTCHA_SITE_KEY` and `CAPTCHA_SECR
```php
$request->validate([
'email' => ['required', 'email'],
- 'captcha' => ['captcha'],
+ 'captcha' => ['laranail_captcha'],
]);
```
diff --git a/composer.json b/composer.json
index 9e1b895..db0fc4e 100644
--- a/composer.json
+++ b/composer.json
@@ -54,7 +54,7 @@
"laranail/console": "^0.1",
"laranail/db-tools": "^0.1",
"laranail/enumerator": "^0.1",
- "laranail/package-tools": "^0.1",
+ "laranail/package-tools": "^0.1.3",
"psr/clock": "^1.0",
"psr/log": "^3.0"
},
diff --git a/docs/architecture.md b/docs/architecture.md
index 14c0d56..3023ba0 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -76,6 +76,24 @@ very different setting from a missing one.
One consequence to know: changing captcha config at runtime needs the service forgotten from the
container, because the policy was already read.
+
+## Public names carry the vendor
+
+Laravel keeps Blade component aliases, container aliases, validation rules and view namespaces in
+flat, host-owned maps, where a second claimant silently replaces the first. So:
+
+| Surface | Name | Deprecated alias (removed no earlier than the next minor after 0.1) |
+|---|---|---|
+| Blade components | ``, `::js`, `::container` | ``, ``, `` |
+| Container alias | `laranail.captcha` (and `CaptchaService::class`) | `captcha` |
+| Validation rule | `laranail_captcha` | `captcha` |
+| View namespace | `laranail/captcha` (canonical), `laranail-captcha` (kept, not deprecated) | |
+| Route name | `laranail.captcha.challenge` | |
+
+Each deprecated alias still works. The tags and the rule raise one `E_USER_DEPRECATED` notice; the
+container alias cannot, because the container offers no hook on alias resolution.
+`tests/Feature/NamingConventionTest.php` asserts all of it against the live registries.
+
---
[← Docs index](../README.md#documentation)
diff --git a/docs/configuration.md b/docs/configuration.md
index f857054..dd34eee 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -84,7 +84,7 @@ Leave `hmac_key` null on the self-hosted providers and a key is derived from `AP
## `widget`
`theme`, `size`, `language` and `nonce`, applied to whichever provider is active. Set `nonce` and
-pass one to `` to keep a strict CSP without `unsafe-inline`.
+pass one to `` to keep a strict CSP without `unsafe-inline`.
## `bot_management`
diff --git a/docs/getting-started.md b/docs/getting-started.md
index 71480c6..33b8738 100644
--- a/docs/getting-started.md
+++ b/docs/getting-started.md
@@ -7,7 +7,7 @@ A protected form and a verified submission, in one page.
```blade
```
@@ -15,7 +15,7 @@ A protected form and a verified submission, in one page.
```php
$request->validate([
'email' => ['required', 'email'],
- 'captcha' => ['captcha'],
+ 'captcha' => ['laranail_captcha'],
]);
```
diff --git a/docs/migration.md b/docs/migration.md
index cdb2656..7748c8a 100644
--- a/docs/migration.md
+++ b/docs/migration.md
@@ -10,7 +10,10 @@ From `rahul900day/laravel-captcha`, and from the `laranail/toolkit` captcha modu
```
`Rahul900day\Captcha\` becomes `Simtabi\Laranail\Captcha\`. The `Captcha` facade alias and the
-`` / `` tags are unchanged, so your Blade needs no edits.
+`` / `` tags still work, so your Blade needs no edits on day
+one. The tags and the `captcha` string rule are deprecated aliases: move to
+``, `` and `laranail_captcha` (see
+[Blade components](tools/blade-components.md) and [the validation rule](tools/validation-rules.md)).
### Read these before deploying
diff --git a/docs/providers.md b/docs/providers.md
index 6640c1c..37861fa 100644
--- a/docs/providers.md
+++ b/docs/providers.md
@@ -43,9 +43,9 @@ to choose — so no provider fact lives in two places.
- **reCAPTCHA v3 / Enterprise** need an action name (`providers.recaptcha.action`), and Enterprise
needs a `project_id` alongside an API key as the secret.
-- **reCAPTCHA v3 and v2-invisible have nothing to click.** `` wires the execution for
+- **reCAPTCHA v3 and v2-invisible have nothing to click.** `` wires the execution for
you: it intercepts the enclosing form's submit once, mints the token and replays the submit. If
- you place `` by hand instead, you have to call `grecaptcha.execute()`
+ you place `` by hand instead, you have to call `grecaptcha.execute()`
yourself, or the form submits with no token at all.
- **Arkose** needs a `client` extra — its verify endpoint is per-customer
(`{client}-verify.arkoselabs.com`).
diff --git a/docs/recipes/protect-a-livewire-form.md b/docs/recipes/protect-a-livewire-form.md
index 7ed13c8..16f8c1c 100644
--- a/docs/recipes/protect-a-livewire-form.md
+++ b/docs/recipes/protect-a-livewire-form.md
@@ -6,7 +6,7 @@ The most-asked question about the package this replaced, and the one it never an
@@ -58,7 +58,7 @@ new class extends Component {
?>
```
diff --git a/docs/tools/blade-components.md b/docs/tools/blade-components.md
index 88a1342..e8a49d4 100644
--- a/docs/tools/blade-components.md
+++ b/docs/tools/blade-components.md
@@ -4,16 +4,16 @@ Three components. Most forms need only the first.
| Tag | Renders |
|---|---|
-| `` | Everything — script and widget, or a server-rendered question |
-| `` | The active provider's script tag alone |
-| `` | The widget mount point alone |
+| `` | Everything — script and widget, or a server-rendered question |
+| `` | The active provider's script tag alone |
+| `` | The widget mount point alone |
-## ``
+## ``
```blade
```
@@ -27,33 +27,38 @@ and widget div.
The split exists because asking someone to place two tags correctly is the difference between a
package that gets used and one that gets copied wrong from Stack Overflow.
-## `` and ``
+## `` and ``
For layouts that want the script in `` and the widget further down:
```blade
-
+
```
-These are the tags the package has always documented, so markup written against the original
-integration keeps working — the migration is a namespace change rather than a sweep through every
-Blade file.
+## Deprecated bare tags
+
+``, `` and `` are the tags the package
+documented before 0.1, and they still render the same components. They are deprecated aliases,
+removed no earlier than the next minor after 0.1: Blade's component aliases are one flat,
+host-owned map, so a bare `captcha` tag is one sibling package away from being silently replaced.
+Each raises one `E_USER_DEPRECATED` notice, when a template using it compiles. Replace them with
+the `laranail-captcha::` tags above.
## Providers with nothing to click
reCAPTCHA v3 and v2-invisible mint their token from `grecaptcha.execute()` rather than from a
-checkbox. `` handles that: it intercepts the enclosing form's submit once, mints the
+checkbox. `` handles that: it intercepts the enclosing form's submit once, mints the
token into a hidden `captcha` field and replays the submit.
-That is why the all-in-one tag is worth preferring. `` alone renders an empty
+That is why the all-in-one tag is worth preferring. `` alone renders an empty
div for those two providers, and the form submits with no token — the failure looks like the captcha
simply not working, with nothing in the logs.
@@ -73,7 +78,7 @@ and a JavaScript identifier.
## Content Security Policy
```blade
-
+
```
Emitted on the script tag, so a strict CSP does not need `unsafe-inline`.
diff --git a/docs/tools/recaptcha.md b/docs/tools/recaptcha.md
index 2b34c8e..08c7529 100644
--- a/docs/tools/recaptcha.md
+++ b/docs/tools/recaptcha.md
@@ -16,9 +16,9 @@ reading `tokenProperties.valid` and `riskAnalysis.score`.
## v3 and v2-invisible have nothing to click
-Their token only exists once `grecaptcha.execute()` has run. `` wires that: it
+Their token only exists once `grecaptcha.execute()` has run. `` wires that: it
intercepts the enclosing form's submit once, mints the token and replays the submit. Place
-`` by hand instead and the form submits with no token at all — a failure
+`` by hand instead and the form submits with no token at all — a failure
that looks like the captcha simply not working, with nothing in any log.
## The score is enforced, not reported
diff --git a/docs/tools/testing.md b/docs/tools/testing.md
index 81f5484..08fc6fd 100644
--- a/docs/tools/testing.md
+++ b/docs/tools/testing.md
@@ -170,7 +170,7 @@ while the entire suite stays green. Nothing else here can see that.
`.github/workflows/install.yml` builds the dist with `git archive` (exactly how Composer builds one
from a tag), asserts both directions — that nothing the runtime needs was stripped, and that
`tests/`, `docs/` and `.github/` did not leak into it — then installs the result into a real Laravel
-application, runs `laranail::captcha.doctor`, renders `` and publishes every advertised
+application, runs `laranail::captcha.doctor`, renders `` and publishes every advertised
tag, checking the files landed. `vendor:publish` exits zero for a tag that does not exist, so each
tag is verified by its output rather than its exit code.
diff --git a/docs/tools/turnstile.md b/docs/tools/turnstile.md
index 5cfe4cc..2c8fee5 100644
--- a/docs/tools/turnstile.md
+++ b/docs/tools/turnstile.md
@@ -25,7 +25,7 @@ works here the day they ship it without a release from us.
| `theme` | `auto` · `light` · `dark` | |
**For a full-width widget set `size` to `flexible`** — via `CAPTCHA_SIZE=flexible`, the
-`captcha.widget.size` config key, or per-widget with ``.
+`captcha.widget.size` config key, or per-widget with ``.
Turnstile sizes itself to the container, so the container also needs a width; it is not a fixed
`100%` on the widget itself.
diff --git a/docs/tools/validation-rules.md b/docs/tools/validation-rules.md
index bc4bac5..9544945 100644
--- a/docs/tools/validation-rules.md
+++ b/docs/tools/validation-rules.md
@@ -3,7 +3,7 @@
One rule, two forms, and one property that matters more than the rest.
```php
-$request->validate(['captcha' => ['captcha']]);
+$request->validate(['captcha' => ['laranail_captcha']]);
```
```php
@@ -13,6 +13,15 @@ $request->validate(['captcha' => [new Captcha]]);
$request->validate(['captcha' => [Captcha::for('login')]]);
```
+The string rule is `laranail_captcha`, spelled the way `laranail/validation` spells its rules
+(`laranail_iban`): the validator's rule map is flat and host-owned, and an underscore survives
+Laravel's studly/snake round trip, so the message key is the rule name as written. On failure it
+reports the rule's own translated message.
+
+**The bare `captcha` rule is a deprecated alias**, removed no earlier than the next minor after
+0.1. It validates exactly as before, implicit included, and raises one `E_USER_DEPRECATED` notice
+per process.
+
## It is implicit
A non-implicit rule is **skipped entirely when the field is absent from the request**. Omitting the
@@ -26,7 +35,7 @@ because an application can reach the rule either way.
## Pairing with `required`
Harmless. Laravel stops validating an attribute once an implicit rule on it has failed, so
-`['required', 'captcha']` on a missing field produces one message rather than two.
+`['required', 'laranail_captcha']` on a missing field produces one message rather than two.
## Binding to an action
diff --git a/src/Commands/InstallCommand.php b/src/Commands/InstallCommand.php
index 50783e9..b9ff48e 100644
--- a/src/Commands/InstallCommand.php
+++ b/src/Commands/InstallCommand.php
@@ -63,7 +63,7 @@ public function handle(): int
}
$this->services->display()->info(sprintf(
- 'Active provider: %s. Drop in a form and add \'captcha\' => \'captcha\' to its rules.',
+ 'Active provider: %s. Drop in a form and add \'captcha\' => \'laranail_captcha\' to its rules.',
$this->activeProvider(),
));
diff --git a/src/Providers/CaptchaServiceProvider.php b/src/Providers/CaptchaServiceProvider.php
index 630fde6..3c694e2 100644
--- a/src/Providers/CaptchaServiceProvider.php
+++ b/src/Providers/CaptchaServiceProvider.php
@@ -12,6 +12,7 @@
use Illuminate\Contracts\Config\Repository;
use Illuminate\Contracts\Events\Dispatcher;
use Simtabi\Laranail\Package\Tools\Package;
+use Illuminate\View\Compilers\BladeCompiler;
use Simtabi\Laranail\Captcha\AdapterFactory;
use Simtabi\Laranail\Captcha\Enums\Provider;
use Illuminate\Contracts\Foundation\Application;
@@ -36,10 +37,13 @@
use Simtabi\Laranail\Captcha\View\Components\Container;
use Simtabi\Laranail\Captcha\Actions\ResolveCredentials;
use Simtabi\Laranail\Captcha\Commands\CacheClearCommand;
+use Simtabi\Laranail\Captcha\Support\DeprecationNotices;
use Simtabi\Laranail\Captcha\Listeners\LogCaptchaOutcome;
use Simtabi\Laranail\Captcha\Listeners\ResetCaptchaState;
use Simtabi\Laranail\Captcha\BotManagement\NullBotManager;
use Simtabi\Laranail\Captcha\Rules\Captcha as CaptchaRule;
+use Simtabi\Laranail\Captcha\View\DeprecatedComponentTags;
+use Illuminate\Validation\Validator as ValidationValidator;
use Simtabi\Laranail\Captcha\Actions\GuardProductionSafety;
use Simtabi\Laranail\Captcha\Contracts\BotManagementAdapter;
use Simtabi\Laranail\Captcha\ValueObjects\VerificationPolicy;
@@ -56,6 +60,9 @@
final class CaptchaServiceProvider extends PackageServiceProvider
{
+ /** The vendor-scoped name of the string validation rule. */
+ public const string VALIDATION_RULE = 'laranail_captcha';
+
public function configurePackage(Package $package): void
{
$package
@@ -64,11 +71,15 @@ public function configurePackage(Package $package): void
->hasConfigFile('captcha')
->hasViews('laranail-captcha')
->hasTranslations('laranail-captcha')
- // Explicit aliases, not a component namespace. A namespace resolves as
- // ``; the tags this package has always documented, and the ones every
- // existing application's markup already contains, are `` and
- // ``. Keeping them is what makes the migration a namespace
- // change rather than a sweep through every blade file.
+ // The components live under the package prefix -- ``,
+ // ``, `` -- because Blade's alias
+ // map is flat and host-owned, and a bare `captcha` tag is one sibling package away from being
+ // silently replaced.
+ ->hasBladeComponentNamespace('Simtabi\\Laranail\\Captcha\\View\\Components', 'laranail-captcha')
+ // @deprecated The bare tags every existing application's markup contains -- ``,
+ // ``, `` -- keep rendering the same classes,
+ // and announce themselves once when a template using them compiles
+ // (DeprecatedComponentTags). Removed no earlier than the next minor after 0.1.
->hasBladeComponentAliases([
'captcha' => CaptchaComponent::class,
'captcha-js' => Js::class,
@@ -199,6 +210,12 @@ public function packageRegistered(): void
);
});
+ // Container aliases share one flat, host-owned map, so the package's own name carries the vendor.
+ $this->app->alias(CaptchaService::class, 'laranail.captcha');
+
+ // @deprecated The bare `captcha` alias is removed no earlier than the next minor after 0.1; resolve
+ // `laranail.captcha`, CaptchaService::class or the facade. The container offers no hook on
+ // alias resolution, so it cannot announce itself; the deprecation is documented.
$this->app->alias(CaptchaService::class, 'captcha');
$this->app->singleton(
@@ -237,6 +254,7 @@ public function packageRegistered(): void
public function packageBooted(): void
{
$this->registerValidationRule();
+ $this->registerDeprecatedComponentTagNotice();
$this->registerChallengeRoute();
$this->registerOctaneReset();
$this->registerLogging();
@@ -373,7 +391,7 @@ private function registerChallengeRoute(): void
}
/**
- * Register the `captcha` string rule as **implicit**.
+ * Register the `laranail_captcha` string rule as **implicit**, and the bare `captcha` rule beside it.
*
* Deliberately not `$package->hasValidationRule()`. That helper registers through
* `Validator::extend`, and a non-implicit rule is skipped entirely when the field is missing
@@ -381,17 +399,65 @@ private function registerChallengeRoute(): void
* exactly what an attacker does, and the old package let those submissions through.
*
* `extendImplicit` also gets the pairing with `required` right. Laravel stops validating an
- * attribute once an implicit rule on it has failed, so `['required', 'captcha']` on an absent
- * field reports one message rather than two.
+ * attribute once an implicit rule on it has failed, so `['required', 'laranail_captcha']` on an
+ * absent field reports one message rather than two.
+ *
+ * The rule map is flat and host-owned, so the package's name carries the vendor, spelled the way
+ * `laranail/validation` spells its aliases (`laranail_iban`): an underscore survives Laravel's
+ * studly/snake round trip, so the message key is the rule name as written. The scoped rule hands
+ * over the rule's own translated message.
*/
private function registerValidationRule(): void
{
Validator::extendImplicit(
- 'captcha',
+ self::VALIDATION_RULE,
// Run through Laravel's own validator so the ValidationRule contract, and the
// rule's message resolution, are honoured natively rather than reimplemented.
- static fn (string $attribute, mixed $value): bool => validator([$attribute => $value], [$attribute => [new CaptchaRule]])->passes(),
+ static function (string $attribute, mixed $value, array $parameters, ValidationValidator $validator): bool {
+ $inner = validator([$attribute => $value], [$attribute => [new CaptchaRule]]);
+
+ if ($inner->passes()) {
+ return true;
+ }
+
+ $message = $inner->errors()->first($attribute);
+
+ if ($message !== '') {
+ $validator->setCustomMessages([$attribute . '.' . self::VALIDATION_RULE => $message]);
+ }
+
+ return false;
+ },
);
+
+ // @deprecated The bare `captcha` rule is removed no earlier than the next minor after 0.1; use
+ // `laranail_captcha` or `new Captcha`. It validates exactly as before and raises one
+ // E_USER_DEPRECATED notice per process.
+ Validator::extendImplicit(
+ 'captcha',
+ static function (string $attribute, mixed $value): bool {
+ DeprecationNotices::once(
+ 'rule:captcha',
+ 'laranail/captcha: the "captcha" validation rule is deprecated and will be removed no earlier than the next minor after 0.1; use "' . self::VALIDATION_RULE . '" or new Captcha.',
+ );
+
+ return validator([$attribute => $value], [$attribute => [new CaptchaRule]])->passes();
+ },
+ );
+ }
+
+ /**
+ * Announce the bare component tags once, when a template that uses them compiles.
+ *
+ * Blade's alias map has no hook on resolution, but every template passes through the
+ * precompilers before it is compiled, and a compiled template is cached -- so this costs one
+ * pattern match per compile, not per render, and never alters the template.
+ */
+ private function registerDeprecatedComponentTagNotice(): void
+ {
+ $this->callAfterResolving('blade.compiler', static function (BladeCompiler $blade): void {
+ $blade->precompiler(new DeprecatedComponentTags);
+ });
}
}
diff --git a/src/Support/DeprecationNotices.php b/src/Support/DeprecationNotices.php
new file mode 100644
index 0000000..4e0132b
--- /dev/null
+++ b/src/Support/DeprecationNotices.php
@@ -0,0 +1,33 @@
+ */
+ private static array $raised = [];
+
+ /** Raise the notice under the given key, unless that key has already been raised. */
+ public static function once(string $key, string $message): void
+ {
+ if (isset(self::$raised[$key])) {
+ return;
+ }
+
+ self::$raised[$key] = true;
+
+ trigger_error($message, E_USER_DEPRECATED);
+ }
+
+ /** Forget which notices were raised. For tests. */
+ public static function forget(): void
+ {
+ self::$raised = [];
+ }
+}
diff --git a/src/View/Components/Captcha.php b/src/View/Components/Captcha.php
index a46912c..419c4b8 100644
--- a/src/View/Components/Captcha.php
+++ b/src/View/Components/Captcha.php
@@ -12,17 +12,17 @@
use Simtabi\Laranail\Captcha\Adapters\Math\MathProblem;
/**
- * `` — the whole thing, in one tag.
+ * `` (deprecated alias ``) — the whole thing, in one tag.
*
- * Drop it inside a form, add `'captcha' => 'captcha'` to the validation rules, and you are done —
+ * Drop it inside a form, add `'captcha' => 'laranail_captcha'` to the validation rules, and you are done —
* whichever of the eleven providers is configured, with or without JavaScript, with or without an
* account anywhere. Switching provider later is a config line and nothing else; this markup does
* not change.
*
- * `` and `` remain for layouts that need the script in
- * `` and the widget further down. This exists because most forms do not, and asking someone
- * to place two tags correctly is the difference between a package that gets used and one that gets
- * copied from Stack Overflow.
+ * `` and `` remain for layouts that
+ * need the script in `` and the widget further down. This exists because most forms do not,
+ * and asking someone to place two tags correctly is the difference between a package that gets used
+ * and one that gets copied from Stack Overflow.
*/
final class Captcha extends Component
{
@@ -53,7 +53,7 @@ public function render(): View
$attributes['data-size'] = $this->size;
}
- return view('laranail-captcha::components.captcha', [
+ return view('laranail/captcha::components.captcha', [
'widget' => $widget,
'widgetAttributes' => $attributes,
'scriptUrl' => $widget->scriptUrl,
diff --git a/src/View/Components/Container.php b/src/View/Components/Container.php
index 480ce18..434230d 100644
--- a/src/View/Components/Container.php
+++ b/src/View/Components/Container.php
@@ -10,7 +10,7 @@
use Simtabi\Laranail\Captcha\Services\CaptchaService;
/**
- * `` — where the active provider's widget renders.
+ * `` (deprecated alias ``) — where the active provider's widget renders.
*
* Each instance gets its own generated id, so two forms on one page work. The old implementation
* had no ids and its callback reached for `document.querySelector('.cf-turnstile')`, which finds
@@ -42,7 +42,7 @@ public function render(): View
$attributes['data-size'] = $this->size;
}
- return view('laranail-captcha::components.container', [
+ return view('laranail/captcha::components.container', [
'widget' => $this->widget,
'widgetAttributes' => $attributes,
]);
diff --git a/src/View/Components/Js.php b/src/View/Components/Js.php
index 24a0bf8..e01908b 100644
--- a/src/View/Components/Js.php
+++ b/src/View/Components/Js.php
@@ -10,7 +10,7 @@
use Simtabi\Laranail\Captcha\Services\CaptchaService;
/**
- * `` — the active provider's script tag.
+ * `` (deprecated alias ``) — the active provider's script tag.
*
* **Returns a View, never a string.** A component whose `render()` returns a string has that
* string written to a file and compiled as a Blade template
@@ -35,7 +35,7 @@ public function render(): View
$captcha = app(CaptchaService::class);
$widget = $captcha->widget();
- return view('laranail-captcha::components.js', [
+ return view('laranail/captcha::components.js', [
'scriptUrl' => $widget->scriptUrl,
'nonce' => $this->nonce,
'lang' => Locale::sanitise($this->lang),
diff --git a/src/View/DeprecatedComponentTags.php b/src/View/DeprecatedComponentTags.php
new file mode 100644
index 0000000..6d6b961
--- /dev/null
+++ b/src/View/DeprecatedComponentTags.php
@@ -0,0 +1,46 @@
+`, `` and
+ * `` tags, once each per process, and returns the template unchanged.
+ *
+ * The tags still render: they remain registered as aliases of the same component classes. They are
+ * deprecated in favour of ``, `::js` and `::container`, and removed no
+ * earlier than the next minor after 0.1.
+ */
+final class DeprecatedComponentTags
+{
+ /** bare tag => scoped tag */
+ public const array TAGS = [
+ 'captcha' => 'laranail-captcha::captcha',
+ 'captcha-js' => 'laranail-captcha::js',
+ 'captcha-container' => 'laranail-captcha::container',
+ ];
+
+ public function __invoke(string $template): string
+ {
+ // Precompilers run after Blade has compiled component tags, so the tag is read back from the
+ // name the compiled component carries: `@component('…\Js', 'captcha-js', [...])`.
+ if (! str_contains($template, "', 'captcha")) {
+ return $template;
+ }
+
+ if (preg_match_all("/@component\\('[^']+', '(captcha(?:-js|-container)?)'/", $template, $matches) > 0) {
+ foreach (array_unique($matches[1]) as $tag) {
+ DeprecationNotices::once('tag:' . $tag, sprintf(
+ 'laranail/captcha: the component tag is deprecated and will be removed no earlier than the next minor after 0.1; use .',
+ $tag,
+ self::TAGS[$tag],
+ ));
+ }
+ }
+
+ return $template;
+ }
+}
diff --git a/tests/Feature/NamingConventionTest.php b/tests/Feature/NamingConventionTest.php
new file mode 100644
index 0000000..7405a0c
--- /dev/null
+++ b/tests/Feature/NamingConventionTest.php
@@ -0,0 +1,130 @@
+ */
+function captureCaptchaDeprecations(callable $callback): array
+{
+ $notices = [];
+
+ set_error_handler(static function (int $level, string $message) use (&$notices): bool {
+ $notices[] = $message;
+
+ return true;
+ }, E_USER_DEPRECATED);
+
+ try {
+ $callback();
+ } finally {
+ restore_error_handler();
+ }
+
+ return $notices;
+}
+
+beforeEach(function (): void {
+ DeprecationNotices::forget();
+});
+
+it('registers its components under the package prefix, with the bare tags listed as deprecated', function (): void {
+ expect($this->assertBladeComponentsScoped(captchaScope(), deprecated: ['captcha', 'captcha-js', 'captcha-container']))
+ ->toContain('laranail-captcha');
+});
+
+it('resolves the scoped tags to the same classes as the deprecated ones', function (string $scoped, string $bare, string $class): void {
+ $compiler = new ComponentTagCompiler(
+ Blade::getClassComponentAliases(),
+ Blade::getClassComponentNamespaces(),
+ Blade::getFacadeRoot(),
+ );
+
+ expect($compiler->componentClass("laranail-captcha::{$scoped}"))->toBe($class)
+ ->and($compiler->componentClass($bare))->toBe($class);
+})->with([
+ ['captcha', 'captcha', CaptchaComponent::class],
+ ['js', 'captcha-js', Js::class],
+ ['container', 'captcha-container', Container::class],
+]);
+
+it('renders the scoped all-in-one tag', function (): void {
+ expect(Blade::render(''))->toContain('data-captcha-config=');
+});
+
+it('announces a bare tag once, when a template using it compiles', function (): void {
+ $notices = captureCaptchaDeprecations(static function (): void {
+ Blade::compileString('');
+ Blade::compileString('');
+ Blade::compileString('');
+ });
+
+ expect($notices)->toHaveCount(1)
+ ->and($notices[0])->toContain('');
+});
+
+it('scopes its container alias, with the bare one listed as deprecated', function (): void {
+ expect($this->assertContainerAliasesScoped(captchaScope(), deprecated: ['captcha']))
+ ->toContain('laranail.captcha')
+ ->and(app('captcha'))->toBe(app('laranail.captcha'))
+ ->and(app('laranail.captcha'))->toBe(app(CaptchaService::class));
+});
+
+it('registers the scoped and the deprecated validation rules as implicit', function (): void {
+ $implicit = new ReflectionProperty(Validator::getFacadeRoot(), 'implicitExtensions')
+ ->getValue(Validator::getFacadeRoot());
+
+ expect($implicit)->toHaveKeys([CaptchaServiceProvider::VALIDATION_RULE, 'captcha'])
+ ->and(CaptchaServiceProvider::VALIDATION_RULE)->toBe('laranail_captcha');
+});
+
+it('fails an absent field under the scoped rule with the rule\'s own message', function (): void {
+ $validator = Validator::make([], ['captcha' => ['laranail_captcha']]);
+
+ expect($validator->fails())->toBeTrue()
+ ->and($validator->errors()->first('captcha'))->toBe(__('laranail-captcha::validation.missing-response'));
+});
+
+it('still runs the bare rule, and announces it once', function (): void {
+ $notices = captureCaptchaDeprecations(static function (): void {
+ expect(Validator::make([], ['captcha' => ['captcha']])->fails())->toBeTrue()
+ ->and(Validator::make([], ['captcha' => ['captcha']])->fails())->toBeTrue();
+ });
+
+ expect($notices)->toHaveCount(1)
+ ->and($notices[0])->toContain('laranail_captcha');
+});
+
+it('registers both view namespace forms over the same paths', function (): void {
+ expect($this->assertViewNamespacesScoped(captchaScope('resources'), atLeast: 2))
+ ->toContain('laranail/captcha', 'laranail-captcha');
+
+ $hints = View::getFinder()->getHints();
+
+ expect($hints['laranail/captcha'])->toBe($hints['laranail-captcha']);
+});