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
@csrf - + @@ -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
@csrf - + ``` @@ -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
@csrf - + ``` @@ -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 - +
@csrf - + ``` -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']); +});