[12.x] Add Eloquent Factory only() to give callers fine-grained control over which related factories are expanded in tests#59200
Open
forrestedw wants to merge 7 commits intolaravel:12.xfrom
Conversation
Introduces Factory::only(...$relationships) to selectively include factory-defined relationships while nulling out all others. Supports dot-notation for nested relationship control, a $required property for always-included relationships, and respects existing withoutParents()/for() behaviour. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
…order States — whether they set a concrete FK, a factory instance, or a scalar — must always be respected, regardless of whether they are applied before or after only(). The previous state-based approach (building only() as a state closure) ran in the middle of the state queue and could not distinguish a factory set by a prior state from the definition's own factory default, because the initial carry in getRawAttributes() *is* definition() and both values are factory objects. Fix: make only() a first-class factory property (like expandRelationships) and apply the filter in getRawAttributes() after all states have run. Because definition() is now called exactly once, its factory instances can be used as strict-identity sentinels: $attrs[$key] === $definition[$key] → definition default, no state touched it $attrs[$key] !== $definition[$key] → state (or for()) explicitly set it → preserve The separate $forAttributes guard is removed: parentResolvers() always runs first, so for() FK values are already concrete (≠ the definition factory) by the time applyOnlyFilter() runs. Adds factory-in-state tests for both orderings (before and after only()). Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
… factories Two new opt-in settings on Factory, each resolvable via a custom callback, a static flag, or config/database.php (precedence in that order, default false): 1. only_by_default / Factory::onlyByDefault() / Factory::resolveOnlyByDefaultUsing() Applies only() globally to every factory without needing to chain it. The $required property per factory class is still respected. 2. infer_required_foreign_keys / Factory::inferRequiredForeignKeys() / Factory::resolveInferRequiredForeignKeysUsing() When only() is active, automatically adds BelongsTo relationships whose FK column is NOT NULL in the schema to the effective whitelist. Uses SchemaBuilder::getColumns() — works across MySQL, MariaDB, PostgreSQL, SQLite, and SQL Server. Results are cached per model class for the process lifetime (not cleared by flushState()) so one DB query covers the whole test suite run. Fails gracefully when the schema cannot be inspected. Resolution order for each flag: resolver callback > static property > config > false (hardcoded default) The static properties are ?bool: null defers to config/resolver. flushState() resets statics and resolvers to null; the schema cache is left intact. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
|
Thanks for submitting a PR! Note that draft PRs are not reviewed. If you would like a review, please mark your pull request as ready for review in the GitHub user interface. Pull requests that are abandoned in draft may be closed due to inactivity. |
Wraps config() calls in a try/catch via resolveFactoryConfig() so factories work in bare PHPUnit setups where no application container is bound. Extracts flag resolution into isOnlyByDefault() and shouldInferRequiredForeignKeys(). Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
…actory attribute Add #[UseFactory(FactoryTestUserFactory::class)] to FactoryTestUser so that FactoryTestUser::factory() resolves correctly regardless of test execution order. Previously the tests implicitly depended on a guessFactoryNamesUsing resolver registered by an earlier test persisting via static state; now that Factory::flushState() is called in tearDown() this implicit dependency broke in alphabetical test-run order. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
…tions Limit reflection-based method iteration to methods declared on user-defined model classes (the model itself or subclasses of Model), excluding built-in methods from Model and its traits. On a fresh unpersisted model instance, some Eloquent methods call Str::upper() with a null attribute value, producing a deprecation in PHP 8.4. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this PR does
Adds an
only()method toIlluminate\Database\Eloquent\Factories\Factorythat gives callers fine-grained control over which related factories are expanded when building or creating models in tests. It also ships two opt-in global settings —Factory::onlyByDefault()andFactory::inferRequiredForeignKeys()— that make the behaviour automatic across a whole test suite.Problem
Every factory
definition()that includes aBelongsTofactory (e.g.'user_id' => UserFactory::new()) will create that parent model unconditionally, even when the test being written doesn't care about it. On large schemas this silently creates many rows, pollutes assertions, slows down test suites, and produces hard-to-debug failures caused by unexpected related data.withoutParents()exists but is all-or-nothing; there was previously no way to say "create only this specific set of relations and ignore the rest".Solution
->only(...$relationships)— whitelist which related factories in the definition should be expanded. All other factory-typed attributes are set tonull; scalar attributes are left untouched so that states applied before or afteronly()are never clobbered.States always take precedence, regardless of chain order. The method is implemented as a first-class factory property (not another state closure), so
definition()is called exactly once and PHP object identity (===) reliably distinguishes a definition-default factory from one explicitly set by a state:$requiredproperty — per-factory class override for relations that must always surviveonly(), regardless of what the caller passes:Factory::onlyByDefault()— activatesonly()globally so factories strip unwhitelisted relations without any per-factory call. Equivalent to implicitly calling->only()on every factory. The$requiredproperty is still respected. Configurable viaconfig/database.php:Factory::inferRequiredForeignKeys()— whenonly()is active (explicitly or globally), inspects the database schema to findBelongsTorelationships whose FK column isNOT NULLand automatically adds them to the whitelist. This means truly required parents are never accidentally stripped. Schema results are cached per model class for the lifetime of the process — exactly onegetColumns()call per model class per test suite run, regardless of how many factories or assertions use that model. Configurable:Per-factory resolver callbacks — for cases where the global flags need different behaviour on a factory-by-factory basis:
Resolution order for each flag: resolver callback → static flag → config →
false.Why this does not break existing features
only()is purely additive; no existing call sites are changed.onlyByDefaultandinferRequiredForeignKeysdefault tofalse(both in the static property and inconfig/database.php), so existing applications are entirely unaffected unless they opt in.flushState()resets all new static properties and resolvers, matching the existing pattern used byexpandRelationshipsByDefault. The schema cache ($schemaRequiredForeignKeys) is intentionally not reset — the schema does not change between tests.applyOnlyFilterstep runs after all states have been accumulated, so every existing chaining pattern (->state(),->for(),->has(),->recycle(),->count(),->sequence(),->withoutParents()) continues to behave exactly as before whenonly()is not involved.Database support
Schema inspection uses
SchemaBuilder::getColumns(), which normalises the nullable flag across all four Laravel-supported drivers: MySQL/MariaDB, PostgreSQL, SQLite, and SQL Server. If the connection is unavailable or the driver does not support column introspection,resolveRequiredForeignKeyRelations()fails gracefully and returns[], leaving the whitelist unchanged.Defining required foreign keys directly on models and in tests is generally preferable. Factory::inferRequiredForeignKeys() is provided as a convenience for codebases adopting only() via Factory::onlyByDefault() — it means existing tests don't all need updating at once, giving a "fastest path to faster tests" on legacy codebases.
The more likely long-term approach is to configure requirements per model or test as needed. This doesn't take long even on a large codebase: $required only needs defining once per factory, failures are loud and obvious, and the per-test cases are easy to handle with a Foo::factory() → Foo::factory()->only('bar') find-and-replace.