laracraft-tech/laravel-schema-rules
Laravel Schema Rules automatically generates basic Laravel validation rules from your database table schema, giving you a starting point to fine-tune validation. Install as a dev dependency and generate rules for whole tables or specific columns.
Install it as a dev-only dependency — that is the first and most important thing to absorb:
composer require laracraft-tech/laravel-schema-rules --dev
php artisan vendor:publish --tag="schema-rules-config"
Because it lands in require-dev, it will not exist in a composer install --no-dev production build. Treat its output as scaffolding you commit, never as something to call at runtime. No application class should reference the package.
Where to look first, in order:
php artisan schema:generate-rules --help — the real surface is small: a table name, --columns, --create-request/-c, --file, --force.config/schema-rules.php — the only tuning knob is skip_columns (defaults already skip created_at, updated_at, deleted_at).src/ driver classes (MySQL / PostgreSQL / SQLite) if you need to know why a given column produced a given rule.The first use case that pays for itself: point it at a table you already know well and use the output as a diff target against the rules you hand-wrote.
php artisan schema:generate-rules persons
'first_name' => ['required', 'string', 'min:1', 'max:100'],
'address_id' => ['required', 'exists:addresses,id'],
'gender' => ['required', 'string', 'in:m,f,d'],
'graduated' => ['required', 'integer', 'min:1901', 'max:2155'],
'children_count' => ['nullable', 'integer', 'min:0', 'max:255'],
Two things are immediately visible and both matter: foreign keys become exists: rules automatically, and integer columns get database-specific min/max ranges. The second one drives most of the adoption advice below.
The second use case, and arguably the strongest one for this package: a legacy or inherited database with dozens of tables and no validation coverage. Generating a baseline in one command per table beats hand-transcribing max:255 for an afternoon.
Scaffold-then-harden, not generate-and-ship. The generated array is a first draft. The intended loop is: generate → paste → hand-tune → commit → re-run on schema change → diff. Practically, generate straight into a form request and edit in place:
php artisan schema:generate-rules persons -c # app/Http/Requests/StorePersonRequest.php
php artisan schema:generate-rules persons -cf --file Api\\V1\\StorePersonRequest
Note the double backslashes in --file — that is shell escaping for a PHP namespace, and it is the documented form. -cf is the shortcut for --create-request --force. Because --force clobbers the file silently, only use it on a clean working tree; the request file is a normal source file, so let git diff be your safety net.
Split store and update requests deliberately. The generator emits the same rule set regardless of intent, so a generated update request will carry required on every non-nullable column and thus reject legitimate partial PATCH payloads. Convert required → sometimes (or sometimes|required) by hand for update requests, and add the uniqueness rules the schema can't express:
'email' => ['sometimes', 'required', 'string', 'max:255',
Rule::unique('persons', 'email')->ignore($this->route('person'))],
Use --columns to scope per endpoint. For partial resources, admin-only fields, or a quick single-field rule, narrowing the output is faster than trimming the paste:
php artisan schema:generate-rules persons --columns first_name,last_name,email
Make skip_columns earn its keep globally. The defaults cover timestamps, but every app has columns that should never be client-validated: password, remember_token, tenant/team IDs injected server-side, computed totals.
'skip_columns' => ['created_at', 'updated_at', 'deleted_at', 'password', 'remember_token', 'team_id'],
Do this once, before generating across a large schema, otherwise you will be deleting the same columns from every request file. Note the array must be complete — the config replaces the default list, it does not merge with it, so keep the three timestamps in there.
Generate against the driver you deploy on. This is the integration tip that prevents the most subtle breakage. The rules are derived from live schema introspection, so a SQLite test database and a MySQL production database yield different integer ranges, different string length inference, and different type mapping. Run the command against a staging database with the production driver, not against database/database.sqlite. If your local dev is SQLite and prod is Postgres, generate in CI or on a staging shell.
Pair it with a schema-change habit. After a migration that renames, lengthens, or nullifies a column, re-run the command for affected tables and diff the output against the committed request classes. The package has no watch mode and no diffing — the loop is manual, so it needs a trigger, typically "touching a migration means re-checking the matching request."
Multi-database and multi-tenant setups. There is no --connection flag documented; the command introspects the default connection. If a table lives on a non-default connection, either run the command in an environment where that connection is default, or treat the output as advisory and hand-write the rules.
Decimals and floats are a known gap. float, decimal, and double all collapse to ['required', 'numeric'] — no precision, no scale, no range. README says so explicitly. Money columns need decimal:2 and between: added by hand, and plain numeric will happily accept 1e5 and -0.001, which is usually wrong for a price or a body size.
Integer ranges are driver-specific and can be surprising. min:-2147483648, max:2147483647 for a signed integer is correct but noisy in error messages; unsignedTinyInteger gives min:0, max:255. Leave them — they genuinely encode the column's capacity — but be aware that if the schema changes from integer to bigInteger, the rule only updates when you regenerate.
min:1 on string and text is not a bug, but it is a decision. It enforces non-empty. In normal HTTP requests Laravel's ConvertEmptyStringsToNull middleware turns "" into null before validation, so nullable|string|min:1 works as expected. It breaks when you instantiate Validator manually in a job, console command, or test with raw "", because nothing converted it. Remove min:1 if empty string is a legitimate value for a field like bio.
nullable is inferred from the column, not from your forms. An HTML checkbox that is unchecked sends nothing at all; a generated required|boolean will fail. Keep the column nullable in the schema, or handle the boolean cast in the form request with prepareForValidation.
Enum columns become string + in:. That works, but if you are on a modern Laravel with a backed PHP enum, swap the pair for Rule::enum(Gender::class) so the enum stays the single source of truth and refactors break loudly.
exists: rules ignore soft deletes and are table-name-derived. foreignId('address_id')->constrained() produces exists:addresses,id, resolved from the constraint. Consequences: a soft-deleted address still passes validation unless you add a whereNull('deleted_at') clause, and schema-qualified Postgres tables need exists:public.addresses,id. Also note the generated rule has no integer companion, so a garbage string produces a generic "selected address is invalid" rather than a type error.
date is looser than you probably want. birth => ['required', 'date'] accepts "next tuesday" — Laravel's date rule parses through strtotime. For a birth date, add before:today and date_format:Y-m-d unless you genuinely accept arbitrary formats.
Auto-increment primary keys are omitted from the output (the README example has an id column that never appears in the rules). That is the right default and means the output maps cleanly onto validated() — but it also means the generated array is not an exhaustive description of the table, so don't present it as one in docs.
Verify the generated stubs before shipping. Check authorize() in the request class the package writes — a scaffold that returns false or true unconditionally is a placeholder, and for anything but a trivial public endpoint you want a policy there.
Extension points are thin. The config exposes only skip_columns; there is no documented rule-mapping registry, no custom driver hook, no --connection. If you need different type mappings (say, mapping uuid columns to uuid rules, or json to stricter shapes), you are overriding the generated request by hand or forking the package. Given the size of the project (a few hundred stars, no dependents listed, single maintainer plus bots), keep the dependency at arm's length: it is a dev convenience whose output is committed source, so removing the package later costs you nothing but the regeneration workflow.
Don't drive it programmatically. It is require-dev; any code path that calls into it will fatal in a production build. If you want automation — regenerating on schema change in CI — shell out to php artisan schema:generate-rules in a build step and diff the captured output, rather than importing the package's classes into application code.
How can I help you explore Laravel packages today?