Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,46 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
Entries below `Unreleased` are written by CI from the GitHub release body — see
[docs/release.md](docs/release.md). Do not hand-edit released sections.

> **One published release: `v0.1.0`.** The org floors every package at `v0.1.0` while pre-stable, so
> that tag is the only one consumers resolve and it moves as the package moves. Sections under
> *Internal history* below were cut as tags during development and later withdrawn; their content is
> part of `v0.1.0`. They are kept for provenance, not because those versions are installable.

## Unreleased

_Nothing yet._

## v0.1.0 - 2026-08-27

Two breaking changes that landed on `main` during org-wide sweeps and were never written down.
Both are mechanical; the provider rename is codemod-able.

### Changed

- **Breaking. The translation namespace is now the composer package name**, `laranail/validation::`,
where it was `laranail-validation::`.

There is no alias. `hasTranslations()` is called without an argument, so the old namespace is not
registered at all and every key spelled the old way returns *itself* instead of a message — no
exception, no warning, just the raw key rendered wherever a validation error would have been. An
application that never names these keys directly is unaffected; one that overrode a message, or
published the translations, is not.

Published files follow the namespace, so an override now lives at
`lang/vendor/laranail/validation/`. That nesting is where Laravel reads it back from —
`FileLoader::loadNamespaceOverrides()` interpolates the namespace into
`{$path}/vendor/{$namespace}/{$locale}/{$group}.php`.

- **Breaking. The service provider moved** to
`Simtabi\Laranail\Validation\Providers\ValidationServiceProvider`.

Package auto-discovery handles this on its own. Anything that names the class explicitly — a
Testbench `getPackageProviders()`, a manual entry in `config/app.php`, a `testbench.yaml` — fatals
with "class not found" until it is updated. `rector-migrate-0.1.php` rewrites it.

The move brings the package in line with the family convention that every provider sits in a
`Providers/` directory, which Laravel's own skeleton does with `app/Providers`.

- The final re-audit against the 1.0 plan closed four release-gate gaps: the README
Stability section and installation guide now speak in shipped-1.0 terms (both still said
"pre-1.0, pin `^0.1`" after the major); `CREDITS.md` records the Phase-2 datasets (ISO
Expand All @@ -20,6 +56,14 @@ Entries below `Unreleased` are written by CI from the GitHub release body — se
attaches a CycloneDX SBOM via `laranail::package-tools.sbom`, the §12.3 item the release
had shipped without.

- Package paths resolve through `package-tools`' `packagePath()` rather than hand-counted
`__DIR__ . '/../..'` strings, which cannot drift when a file moves.

## Internal history (not published)

These were tagged during development and the tags have since been withdrawn. Nothing here is
separately installable — it all ships inside `v0.1.0` above.

## v1.0.1 - 2026-08-24

### Added
Expand Down
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ Full documentation is at

## Stability

1.0 states what SemVer covers. The **stable surface** is: `FluentRule`,
2.0 states what SemVer covers. The **stable surface** is: `FluentRule`,
`FluentSchema`, `RuleSet` (including its events and `before()`/`after()` hooks), the rule
classes and their constructor signatures, the contracts (`ClientCheckable`,
`PrecognitionSkippable`, `TermList`, `FluentRuleContract`), `Check`, `Regex`,
Expand All @@ -122,8 +122,9 @@ Everything marked `@internal` — the fast-check compiler, the optimizer validat
machinery, everything under `Internal\` — may change in a minor, and an arch test enforces the
boundary. Build on the stable list; the optimizer is an implementation detail behind it.

Constrain to `^1.0` and read [UPGRADING.md](UPGRADING.md) before moving between versions —
`rector-migrate-1.0.php` auto-migrates the mechanical 0.x break. Deprecations post-1.0 are
Constrain to `^0.1` and read [UPGRADING.md](UPGRADING.md) before moving between versions —
`rector-migrate-0.1.php` auto-migrates the mechanical break (the service provider moved into
`Providers/`), and `rector-migrate-1.0.php` still covers the 0.x one. Deprecations post-1.0 are
marked `@deprecated` with the replacement and removal version, kept for at least one minor,
and removed only in the next major. Behaviour-correcting fixes can still mean input an
application previously accepted is now correctly rejected; UPGRADING calls those out.
Expand Down
65 changes: 65 additions & 0 deletions UPGRADING.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,71 @@

Breaking changes, and what to do about them. Versions not listed here need no action.

## v0.1.0 - 2026-08-27

The provider rename below can be applied automatically:

```bash
vendor/bin/rector process app/ --config vendor/laranail/validation/rector-migrate-0.1.php
```

The config declares no paths of its own, so pass them: `app/` at minimum, and usually `tests/` too —
a Testbench `getPackageProviders()` is the single most common place the old class name survives.
`config/` and `bootstrap/` are worth a pass. `testbench.yaml` is not PHP, so Rector will not see it;
grep for the old name there by hand.

The translation-key rename is a string change, which Rector has no clean rule for. It is a
find-and-replace, described below.


### The service provider moved into `Providers/`

| Before | After |
|---|---|
| `Simtabi\Laranail\Validation\ValidationServiceProvider` | `Simtabi\Laranail\Validation\Providers\ValidationServiceProvider` |

**Most applications need no change.** Laravel's package auto-discovery finds the provider through
`composer.json`, which moved with it.

You need this change if you name the class yourself:

- a Testbench `getPackageProviders()` in a test case,
- a manual entry in `config/app.php` or `bootstrap/providers.php`,
- a `testbench.yaml` `providers:` list — worth grepping for specifically, since it is neither
`.php` nor `.json` and a namespace sweep over source files misses it.

Left unchanged, it fatals with "class not found" the moment the provider is registered — at boot,
for the whole application.

### The translation namespace is now `laranail/validation::`

| Before | After |
|---|---|
| `laranail-validation::validation.iban` | `laranail/validation::validation.iban` |
| `lang/vendor/laranail-validation/` | `lang/vendor/laranail/validation/` |

**There is no alias.** The old namespace is not registered at all, so a key spelled the old way
returns *itself* — `laranail-validation::validation.iban` renders where the message should be. No
exception is raised, which is what makes this worth checking for rather than waiting to notice.

Two places to look:

```bash
grep -rn 'laranail-validation::' app/ resources/ config/ lang/
ls lang/vendor/laranail-validation 2>/dev/null # a published override, now read from elsewhere
```

If you published the translations, move the directory:

```bash
mkdir -p lang/vendor/laranail
git mv lang/vendor/laranail-validation lang/vendor/laranail/validation
```

That nesting is not incidental — Laravel interpolates the namespace into the override path itself
(`FileLoader::loadNamespaceOverrides()` reads
`{$path}/vendor/{$namespace}/{$locale}/{$group}.php`), so the slash is exactly where it looks.

## v1.0.0 - 2026-08-24

The mechanical break below (`getEachRules()`) can be applied automatically:
Expand Down
16 changes: 8 additions & 8 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
}
],
"name": "laranail/validation",
"description": "Fluent validation rule builders for Laravel — type-aware rule objects, structured array validation, and an optimized wildcard validator.",
"description": "Fluent validation rule builders for Laravel \u2014 type-aware rule objects, structured array validation, and an optimized wildcard validator.",
"keywords": [
"laravel",
"laranail",
Expand Down Expand Up @@ -91,7 +91,7 @@
"phpstan/phpstan-deprecation-rules": "^2.0.4",
"phpstan/phpstan-phpunit": "^2.0.16",
"phpstan/phpstan-strict-rules": "^2.0.10",
"rector/rector": "^2.4.1",
"rector/rector": "^2.4.1 <2.6.4",
"sandermuller/boost-skills": "^2.5",
"sandermuller/package-boost-laravel": "^1.0",
"spaze/phpstan-disallowed-calls": "^4.10",
Expand All @@ -100,11 +100,11 @@
"tomasvotruba/type-coverage": "^2.3.4"
},
"suggest": {
"laranail/email": "^0.1 — maintained disposable-domain and role-account lists plus a production DNS resolver, bound over the Contracts\\Email\\* fallbacks this package ships. Nothing you call changes.",
"laranail/phone": "^0.1 — required by FluentRule::phone() and Rules\\Telecom\\Phone. Suggested rather than required because it carries libphonenumber's numbering-plan metadata, which a project validating only strings and dates should not have to install.",
"nikic/php-parser": "^5.0 — required by Simtabi\\Laranail\\Validation\\Testing\\Arch\\BansFieldRuleTypeMethods, an opt-in Pest/PHPUnit arch helper that bans type-specific method calls on the untyped FluentRule::field() builder.",
"laranail/password-history": "^0.1 — reuse prevention: password()->notReused() appears when installed.",
"laranail/password-tools": "^0.2 — zxcvbn strength scoring and secure generators: password()->strength(3) appears when installed."
"laranail/email": "^0.1 \u2014 maintained disposable-domain and role-account lists plus a production DNS resolver, bound over the Contracts\\Email\\* fallbacks this package ships. Nothing you call changes.",
"laranail/phone": "^0.1 \u2014 required by FluentRule::phone() and Rules\\Telecom\\Phone. Suggested rather than required because it carries libphonenumber's numbering-plan metadata, which a project validating only strings and dates should not have to install.",
"nikic/php-parser": "^5.0 \u2014 required by Simtabi\\Laranail\\Validation\\Testing\\Arch\\BansFieldRuleTypeMethods, an opt-in Pest/PHPUnit arch helper that bans type-specific method calls on the untyped FluentRule::field() builder.",
"laranail/password-history": "^0.1 \u2014 reuse prevention: password()->notReused() appears when installed.",
"laranail/password-tools": "^0.2 \u2014 zxcvbn strength scoring and secure generators: password()->strength(3) appears when installed."
},
"autoload": {
"psr-4": {
Expand Down Expand Up @@ -148,7 +148,7 @@
]
},
"branch-alias": {
"dev-main": "1.0.x-dev"
"dev-main": "0.1.x-dev"
}
},
"config": {
Expand Down
40 changes: 28 additions & 12 deletions docs/release.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,23 +65,39 @@ requests, and weekly, since a tag can also be moved on the remote without any pu

## Cutting the release

1. Tag the release commit with the `v`-prefixed version — `v0.1.1`. Composer reads either
form, and the `v` prefix is what the repo's existing tags, the org convention, and the
tag-currency check (`verify-tag-currency.sh` filters on `^v`) all agree on. This page once
said "bare version"; that sentence contradicted all three and the script never matched it.
Releases are real SemVer points — the pre-1.0 moving-tag model is retired for this package.
2. Create the GitHub release against that tag, titled the same (`v0.1.1`).
3. Write a real description in the release body. Summarise what changed and why, in prose — an
empty body or a bare "see CHANGELOG" is not a release description. This body becomes the
changelog entry, so it is the version's permanent record.
**The changelog comes first, and the tag is the trigger.** `release.yml` fires on the tag push and
*extracts* the `## vX.Y.Z` section out of `CHANGELOG.md` to use as the release body — and `exit 1`s if
no such section exists. So there is one order, and tagging is the last step in it.

1. Write the version's section in `CHANGELOG.md`, headed `## vX.Y.Z - <date>`. Real prose about
what changed and why; this becomes the release body and is the version's permanent record. The
heading has to match `^## (\[)?(v)?X.Y.Z([] ]|$)`, which is what the extractor greps for.
2. Confirm it extracts before you tag anything:
```bash
awk -v ver="X.Y.Z" '$0 ~ "^## (\\[)?(v)?"ver"([] ]|$)" { grab = 1; next } grab && /^## / { exit } grab { print }' CHANGELOG.md
```
Empty output means the release job will fail on its first step.
3. Commit the changelog entry to `main`.
4. Tag the release commit with the `v`-prefixed version — `v0.1.0`. Composer reads either form, and
the `v` prefix is what the repo's existing tags, the org convention, and the tag-currency check
(`verify-tag-currency.sh` filters on `^v`) all agree on. This page once said "bare version"; that
sentence contradicted all three and the script never matched it. Releases are real SemVer points —
the pre-1.0 moving-tag model is retired for this package.
5. Push the tag. Everything else is CI's: it creates the release, injects the benchmark table
between the markers it appends itself, and attaches the SBOM.

> This page used to say "create the GitHub release, then write a description in the body", with
> the changelog written back afterwards. That was the older flow. `update-changelog.yml` still
> exists for it, but it now only backfills a release authored by hand and skips when the section is
> already present — which, on the normal path, it is.

## What CI does afterwards

Publishing the release (`release: released`) fires three workflows:

- **Update Changelog** prepends the release body to `CHANGELOG.md` and commits it back to the
release's target branch. This is why `CHANGELOG.md` is never hand-edited as part of a
release — a manual entry will be duplicated.
- **Update Changelog** backfills `CHANGELOG.md` from the release body, and *only* when the section
is missing — a release authored by hand in the GitHub UI. On the normal path the section is
already there, because it is what the body was built from, and this job does nothing.
- **Release Benchmark** re-runs the benchmark suite against the tagged commit and injects the
results table into the release body, between the `<!-- benchmark-start -->` and
`<!-- benchmark-end -->` markers. Those markers must already be present in the body for the
Expand Down
46 changes: 46 additions & 0 deletions rector-migrate-0.1.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

declare(strict_types=1);

use Rector\Config\RectorConfig;
use Rector\Renaming\Rector\Name\RenameClassRector;

/**
* The v0.1.0 migration set (UPGRADING.md). Run it against an
* application's own code:
*
* vendor/bin/rector process app/ \
* --config vendor/laranail/validation/rector-migrate-0.1.php
*
* It applies the mechanical break only — the service provider moved into a
* `Providers/` sub-namespace, and anything naming the class explicitly needs
* the new name. Auto-discovered registrations need nothing.
*
* The other break, the `laranail-validation::` → `laranail/validation::`
* translation namespace, is a change to string literals. Rector has no clean
* rule for that, and a rule that rewrote arbitrary strings would be worse than
* the find-and-replace UPGRADING.md gives you.
*
* Paths worth passing beyond `app/`: `tests/` (a Testbench
* `getPackageProviders()` is the most common site), `config/` and
* `bootstrap/`. `testbench.yaml` is not PHP and Rector will not see it — grep
* for it by hand.
*/
return RectorConfig::configure()
// Without this the rename lands as a fully-qualified name inline and leaves the old `use`
// sitting above it -- correct, but not what anyone wants to read in their own diff.
//
// `importShortClasses: false` deliberately diverges from this repo's own rector.php, which takes
// the defaults. It gates exactly one thing: ShortClassImportSkipVoter skips importing a class
// only when `substr_count($className, '\\') === 0` -- a global-namespace class such as
// \DateTime. The provider being renamed here carries four separators, so it is untouched by the
// flag and is still imported as a `use`.
//
// The difference is whose code Rector is pointed at. rector.php runs over this repository, where
// importing global classes everywhere is wanted. This config runs over a *consumer's*
// application, where a one-class migration that also rewrites every \DateTime and \Exception in
// their tree buries the rename in an unrelated diff.
->withImportNames(importShortClasses: false, removeUnusedImports: true)
->withConfiguredRule(RenameClassRector::class, [
'Simtabi\Laranail\Validation\ValidationServiceProvider' => 'Simtabi\Laranail\Validation\Providers\ValidationServiceProvider',
]);
Loading