Repository navigation
(feat) Create and save records through relationships - #378
Conversation
There was a problem hiding this comment.
@techmahedy, Reviewed the full diff and CI (all checks green).
Blocking: BindTo::associate() does not check the parent's class.
store() throws InvalidArgumentException for a model of the wrong class, but associate() only reads $parent->{$this->ownerKey}. So $article->author()->associate(new Bio()) silently sets author_id to the bio's id, which is the kind of orphan write this PR is meant to prevent. Please add the same class check and a test for the wrong-class case. CI does not cover this path today.
Non-blocking:
Model::newRelationBuilder()now copies every non-staticBuilderproperty from$related::query()on each relation access. This is what makesquery()overrides apply to relations, but it also adds an extra builder per access and copies any future property automatically. A short comment explaining why it is needed would help.- Relations on an unsaved model now match nothing, where they used to match
NULLforeign keys. This is tested and documented, but it is a behavior change worth a release note.
Once the associate() check is in, this looks good to merge.
@abdulmajidcse please check now |
ok, perfect |
Pull Request Checklist
Description
After this PR, you can now create and save related records through a relationship, and Doppar sets the foreign key for you:
The problem
Doppar had good read-side relationship tools (
embed,embedCount,present,whereLinked, pivotlinkandrelate), but no way to write through a relationship. A relation is a query builder, so$post->comments()->create([...])ran the plain buildercreate():post_id, so it was an orphan.createMany, no way to attach an existing model to a parent, and no way to point a child at a different parent without setting the key by hand.What changed
linkOneandlinkManyrelationscreate(array $attributes)createMany(array $records)Collection. Each record is a separate insert, so callers can wrap it in a transaction.store(Model $related)falseif the save fails.storeMany(iterable $models)store()for each model.firstOrCreate(),updateOrCreate()All writes go through the model's own
save(), so hooks, watches and temporal snapshots run.->withoutHook()->create(...)and->withoutHook()->store(...)skip them.bindTorelationsassociate(Model|int|string $parent)dissociate()null. It does not save. Returns the model.Rules that apply to all writes
LogicException.$creatable. If it is not, the call throws aLogicExceptionthat names the column and the model, and nothing is written. Without this check the insert would silently drop the foreign key and leave an orphan row. It also means a relation can never write a column the model has not opted into.store()throws anInvalidArgumentExceptionfor a model of the wrong class.Related fix: relations of an unsaved model match nothing
While checking the write rules I found an existing bug. A relation on a model with no key yet, such as
(new Author())->articles, matched every row whose foreign key isNULL, becausewhere(fk, '=', null)becomesfk IS NULL. So an unsaved model "owned" every orphan row.linkOne,linkManyandbindTonow match nothing when the key they point at isnull. The write methods already refused an unsaved parent, so reads and writes now agree.Supporting change
Model::isCreatable(string $attribute): boolis new and public. The relation classes use it for the check above.Checklist