Skip to content

Conversation

@thomascorthals
Copy link
Member

A new major version is the perfect opportunity to do some overhaul on the codebase. I've replaced the type hints with actual type declarations on class properties (introduced in PHP 7.4). And I've added union type declarations to method signatures (introduced in PHP 8.0) where this was previously also only done in type hints.

This is a breaking change for anyone extending our codebase, which I've added to the pitfalls. But it shouldn't break much for regular use of the library since I didn't have to change anything in the integration tests and they still pass.

@codecov
Copy link

codecov bot commented Nov 16, 2025

Codecov Report

❌ Patch coverage is 99.44751% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 98.17%. Comparing base (80e85f5) to head (3d85417).

Files with missing lines Patch % Lines
src/Component/Result/Facet/Aggregation.php 50.00% 1 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##           master    #1168   +/-   ##
=======================================
  Coverage   98.16%   98.17%           
=======================================
  Files         404      404           
  Lines       10632    10633    +1     
=======================================
+ Hits        10437    10439    +2     
+ Misses        195      194    -1     
Flag Coverage Δ
unittests 98.17% <99.44%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@mkalkbrenner
Copy link
Member

mkalkbrenner commented Nov 17, 2025

@thomascorthals I agree, we should do that for 7.0.0.
But what do you think? Shouldn't we declare interfaces for every class and use these interfaces as type properties?

@thomascorthals
Copy link
Member Author

But what do you think? Shouldn't we declare interfaces for every class and use these interfaces as type properties?

Hadn't really thought about that. Would add additional work on our side to keep them in sync.

I was actually looking into getting the codebase to PHPStan level 2 and a lot of the errors seem to be caused by type hinting with interfaces that don't define all methods that are present in the implementing class. Here's an example, there are currently dozens like this.

 ------ ---------------------------------------------------------------------------------------------
  Line   Component\Facet\JsonFacetTrait.php (in context of class Solarium\Component\Facet\JsonTerms)
 ------ ---------------------------------------------------------------------------------------------
  140    Call to an undefined method Solarium\Component\Facet\FacetInterface::serialize().
         🪪  method.notFound
 ------ ---------------------------------------------------------------------------------------------

 ------ -----------------------------------------------------------------------------------
  Line   Component\RequestBuilder\FacetSet.php
 ------ -----------------------------------------------------------------------------------
  55     Call to an undefined method Solarium\Component\Facet\FacetInterface::getField().
         🪪  method.notFound
  65     Call to an undefined method Solarium\Component\Facet\FacetInterface::getField().
         🪪  method.notFound
  98     Call to an undefined method Solarium\Component\Facet\FacetInterface::serialize().
         🪪  method.notFound
 ------ -----------------------------------------------------------------------------------

@mkalkbrenner
Copy link
Member

OK, then fix the existing interfaces and avoid further interfaces until required?

@thomascorthals
Copy link
Member Author

thomascorthals commented Nov 20, 2025

I don't think we can "fix" the existing interfaces. I've done the PHPStan level 2 fixes in the code in my own branch because it's easier to just show what it takes: thomascorthals@a4c09bc.

What can't be fixed with type declarations is usually fixable by type hinting. But that doesn't "fix" the interface.

Take the build() method for instance. It's signature in AbstractRequestBuilder asks for a QueryInterface.

    public function build(QueryInterface $query): Request

It can't be more specific than that because every class inheriting from it has a different Query class to build for. For Select it has this signature.

    public function build(QueryInterface|SelectQuery $query): Request

Because of contravariance, we can make the accepted parameters wider. That's what the union type does. However, static analysis doesn't "know" that we're only ever passing SelectQuery and reports all of its methods (setStart(), setRows()) as method.notFound. This isn't completely superfluous though as IDEs are able to provide autocompletion for $query->... with that function signature.

There is a workaround for static analysis if you use intersection types. Because that means narrowing the accepted type for the method, it's not possible to do so in a type declaration. But PHPStan prioritises PHPDoc type hints over the actual type declaration.

    /**
     * Build request for a select query.
     *
     * @param QueryInterface&SelectQuery $query
     *
     * @return Request
     */
    public function build(QueryInterface|SelectQuery $query): Request

This is the best solution I could come up with for the current structure of our codebase. It doesn't break backward compatibility, PHPStan knows what to do, IDEs know what to do, humans get the intent.

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.

2 participants