Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix relation types #1285

Closed
wants to merge 28 commits into from
Closed

Conversation

nagmat84
Copy link

@nagmat84 nagmat84 commented Jun 19, 2022

  • Added or updated tests
  • Documented user facing changes
  • Updated CHANGELOG.md

Resolves #1284

Changes

This PR

  • heavily adds more PHPstan annotations to the Relation-related classes and
  • introduces two, resp. three, new template parameters besides the already existing TRelatedModel:
    • TDeclaringModel
    • TResult
    • TIntermediateModel (only in case of the ...Through...-relations)

Therewith it also resolves issue #1284.

Rationale

A relation links two models with each other which are not necessary identical. The TRelatedModel is the type of model to which the relaton points to. The TDeclaringModel is the type of model which declares the relation (i.e. the "base" of the arrow in the picture).

relation

Note that the roles TRelatedModel and TDeclaringModel are independent of the definition of the foreign key on SQL layer. In the example above, only Post has an attribute topic_id to realize a (1:*)-relation. But the relations hasMany and belongsTo live on the "object space" and each points from its declaring model to the related model.

In its most general form a Relation has a third template type TResult which is not necessarily a model. Typically TResult is either identical to TRelatedModel or a Collection<int, TRelatedModel>. This is true for the Has...One and Has...Many-relations. The latter also shows why TResult must not be restricted to a sub-type of Model.

However, for custom relations which extend Relation this must not be true. For example, instead of an Eloquent collection a custom relation could use another type of container, like a FixedSizeArray<TRelatedModel> which provides higher efficiency if one has a (1:n)-relation for a fixed n. (See #1275 (comment) for a real-world example.) However, TResult could also be something like \DateTime. In the example above, a topic could have two relations HasDateOfLeastRecentPost and HasDateOfMostRecentPost which resolve to the date/time of the least/most recent post. In other words, the result of a relation is a value which depends on the related model(s), but is not necessarily the related model(s).

A Remark on Notation

I chose the term TDeclaringModel over TParentModel intentionally, although Laravel calls this model the "parent model" of the relation in most cases and the corresponding variable is called $parent. See

https://github.com/laravel/framework/blob/9.x/src/Illuminate/Database/Eloquent/Relations/Relation.php#L30-L35

However, it does not do so consistently. In BelongsTo the names are not used according to their usage in the code but are based on the use case. The declaring model is named the "child model" and the "parent" becomes the related model. See

https://github.com/laravel/framework/blob/9.x/src/Illuminate/Database/Eloquent/Relations/BelongsTo.php#L62-L65

Even worse BelongsTo does not apply this change of names consistently, i.e. the related model and the declaring model are both called the parent model. In order to avoid strange constructs like TParentModel $child, I decided to use TDeclaringModel. Moreover, the term "declaring" class has already been used inside the Larastan sources.

This gives the following table

Template parameter Classes Relation, HasMany, HasOne, ..., BelongsToMany Class BelongsTo (only)
TRelatedModel $related $related
TDeclaringModel $parent $child

Breaking changes

The Relation-related classes now require up to four template parameters.

There should be no breaking change for ordinary users which only use the provided relations and create relations via the methods hasMany, hasOne, ..., etc. provided by \Illuminate\Database\Eloquent\Concerns\HasRelationships. The classes ModelRelationsDynamicMethodReturnTypeExtension and RelationDynamicMethodReturnTypeExtension have been adopted to bound all template parameters accordingly.

A breaking change may occur for users which implement their own relation classes and extend from Relation or its sub-classes. They need to bind the newly added template parameters. But I assume this group to be rather small. Otherwise someone else would already have recognized this bug earlier.

@szepeviktor
Copy link
Collaborator

⚠️ We have a computer scientist onboard. 👨🏻‍🔬

@szepeviktor
Copy link
Collaborator

szepeviktor commented Jun 19, 2022

All tests have passed :)

We use FQCN in docblocks.

@nagmat84
Copy link
Author

⚠️ We have a computer scientist onboard. 👨🏻‍🔬

Why the warning sign? Are you afraid of theoretical computer scientists? 😲 (BTW: You guessed correctly, I am indeed a theoretical computer scientist.)

All tests have passed :)

I haven't expected less. I have run them locally first. 😏

We use FQCN in docblocks.

OK. I will change that, but not today. It is getting late. I also still have to update the CHANGELOG.md, but I wanted to see first what you think about this PR in general.

@canvural
Copy link
Collaborator

Hi,

Thank you for this!

While it makes sense to have the declaring class also in the generic types, I do not really see a use case for that. Even in the stubs you only used TDeclaringModel in one or two methods. And one of them is protected. And I don't see any new tests that are using TDeclaringModel

Can you give me a small code snippet where TDeclaringModel is useful?

@szepeviktor
Copy link
Collaborator

szepeviktor commented Jun 24, 2022

All my heart goes out to @nagmat84 ♥️
while crying in my lab time-to-time as business people are driven by measurable value.

@nagmat84
Copy link
Author

nagmat84 commented Jun 25, 2022

While it makes sense to have the declaring class also in the generic types, I do not really see a use case for that.

First of all it fixes this bug: #1284 The bug is caused by the wrong assumption (and in consequence the wrong annotations) that the declaring model is the same as the related model.

Even in the stubs you only used TDeclaringModel in one or two methods. And one of them is protected.

That is simply not true. Let alone BelongsTo uses it five times.

Can you give me a small code snippet where TDeclaringModel is useful?

It becomes useful in the very moment when you are working an real projects which also define their own relations because the default relations don't suffice. In that case one has to extend Relation. That is also the reason why I annotated all protected methods, because they become relevant as soon as one uses custom relations.

If you need examples, just have a look here Lychee: /app/Relations. At the moment we have to write hundreds of phpstan-ignore comments like in /app/Relations/HasManyPhotos::getParent()

@nagmat84
Copy link
Author

nagmat84 commented Jul 2, 2022

I updated all PHPDoc comments to contain FQCN as requested by this comment.

I also added a test case to illustrate why TDeclaringModel is required. However, in doing so I discovered a problem. It seems as if a similar problem to the one reported in #1289 is lurking somewhere. I get the report

Call to an undefined method Builder<TRelatedModel of Model>::...

three times. Obviously, the methods orderBy, union and newQuery are defined. In #1289 @canvural pointed out in this post that the template parameter must not be renamed between inherited classes. This advice solved the problem in #1289, but here the problem is back again.

Maybe, the problem occurs, because the template parameter is called TRelatedModel in the Relation-classes, but TModelClass in Builder. However, I do not know what black magic Larastan does internally that it chokes when template parameters are renamed. Maybe, the cause of the problem is something different here. I would be really grateful, if @canvural could have a look into it. This is out of my wheelhouse.

@nagmat84
Copy link
Author

nagmat84 commented Jul 3, 2022

I believe I fixed the problem even though I have not fully understood what I did. In order to solve the "one-must-not-rename template-parameter-names" I stepped through the source code, watched out for the literal string 'TModelClass' and added 'TRelatedClass' and 'TDeclaringClass'. See here b8331c1. Previously this has already been the case for 'TRelatedClass' at some places, but not consistently.

As a second change I had to remove a condition in EloquentBuilderForwardsCallsExtension which prevented a method call from being forwarded to the underlying query object if the template parameter was bound to the unspecific Model class instead of a more specific user-defined model class. See this commit for the change f660f26.

Obviously, removing the condition is required if one implements custom relations which shall work for all type of models and not only specific models. However, I don't know why the condition has been there in the first place. All tests are still running, hence the condition seems not to have had any impact previously and I assume it is OK to remove it. Unfortunately, the code is missing comments explaining why the original author added this or that condition. Someone with more knowledge about Larastan needs to look into that change thoroughly.

I hope the PR is now fine to be merged.

@szepeviktor
Copy link
Collaborator

szepeviktor commented Jul 3, 2022

@canvural I think we should release it and see what custom relation developers have to say.

@nagmat84
Copy link
Author

nagmat84 commented Jul 3, 2022

I wonder how many there are. Obviously, I am one and I directly crashed into a lot of problems which led to this PR. So either the rest does not care and uses a lot of phpstan-ignore annotations or there are no other developers which use custom relations.

@szepeviktor
Copy link
Collaborator

there are no other developers which use custom relations.

I support one of those developers: having a big phpstan-baseline.neon file.

@nagmat84
Copy link
Author

What is the matter of state? When will this PR be merged? What can I do to push things forward?

@szepeviktor
Copy link
Collaborator

szepeviktor commented Jul 10, 2022

I'm not allowed to answer as I'm also sitting in my lab 🧪 but Larastan must be lazy developer compatible.

p.s. @nagmat84 I would merge it right away

@nagmat84
Copy link
Author

Who is allowed to answer and has the authority to push things forward? @szepeviktor Thanks for your support.

@canvural
Copy link
Collaborator

I'm currently really busy with work, and cannot review it now. I'll do it as soon as I can find some time.

@nagmat84
Copy link
Author

nagmat84 commented Jul 23, 2022

Is it possible to give a rough estimate when this PR will be reviewed or get merged?

@szepeviktor
Copy link
Collaborator

a rough estimate

In two weeks.

@szepeviktor
Copy link
Collaborator

szepeviktor commented Jul 23, 2022

@nagmat84 You can see how we work on the rhythm of commits in master.

@nagmat84
Copy link
Author

There have been two two-weeks cycles since the last comment. What's the progress on this PR?

@canvural
Copy link
Collaborator

canvural commented Sep 1, 2022

There have been two two-weeks cycles since the last comment. What's the progress on this PR?

Hi,

We do not have any cycles of development here. 2 weeks was just an estimate of @szepeviktor I think there.

Anyway, I apologize for the lack of communication here. Past few months were crazy busy for me. Work wise and personal issues.

Only last week I found some time to get through some issues and pull requests. Made a new release with them. And still there are some issues/prs to review left. This is kinda a large PR, so first I spent time on smaller ones.

I'll try to get to this as soon as possible though. Alternatively you can always motivate me through the sponsors: https://github.com/sponsors/canvural 😊

Thank you for understanding.

@nagmat84
Copy link
Author

nagmat84 commented Oct 20, 2022

I'll like to pose my usual question. ;-)

@mad-briller
Copy link
Contributor

mad-briller commented Oct 20, 2022

There should be no breaking change for ordinary users

this is not true, any users who have @return HasOne<Model> in phpdocs above their relations will have to update them, which could definitely be seen as a breaking change.
it's also worth noting that without the phpdoc, phpstan will error with:

Method Model::relation() return type with generic class Illuminate\Database\Eloquent\Relations\HasOne does not specify its types: TRelatedModel  

which forces the addition of such a phpdoc

@nunomaduro
Copy link
Collaborator

We've closed the master branch. Please open a new pull request and target the 2.x branch if you wish to re-open this pull request.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

HasOneOrMany wrongly uses TRelatedModel as the type for the declaring model of the relation
5 participants