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

bug: Model::find() returns incorrect type #7

Closed
kenjis opened this issue Nov 9, 2023 · 18 comments · Fixed by #18
Closed

bug: Model::find() returns incorrect type #7

kenjis opened this issue Nov 9, 2023 · 18 comments · Fixed by #18
Labels
bug Something isn't working

Comments

@kenjis
Copy link

kenjis commented Nov 9, 2023

PHP Version

8.1

PHPStan CodeIgniter Version

v1.4.2.70400

PHPStan Version

1.10.39

What happened?

 ------ ------------------------------------------------------------------------------ 
  Line   app/Models/ThreadModel.php                                                    
 ------ ------------------------------------------------------------------------------ 
  177    Cannot access property $thread_count on array<int, App\Entities\Category>.    
  177    Cannot access property $thread_count on array<int, App\Entities\Category>.    
  178    Cannot access property $post_count on array<int, App\Entities\Category>.      
  183    Cannot access property $id on array<int, App\Entities\Category>.              
  187    Cannot access property $last_thread_id on array<int, App\Entities\Category>.  
  188    Cannot access property $last_thread_id on array<int, App\Entities\Category>.  
  195    Cannot use -- on array|bool|float|int|object|string|null.                     
 ------ ------------------------------------------------------------------------------ 

https://github.com/kenjis/forum-example/blob/37a339f050ee2c40ed452e6570cef188dc0f90a0/app/Models/ThreadModel.php#L173-L195

Minimum Reproduction Script

git clone https://github.com/kenjis/forum-example --branch=add-phpstan-codeigniter
cd forum-example/
composer install
vendor/bin/phpstan

Expected Output

No errors on ThreadModel.

@kenjis kenjis added the bug Something isn't working label Nov 9, 2023
@paulbalandan
Copy link
Collaborator

Except for the last error, the errors are coming because PHPStan does not know what is the type of $data['data']['category_id']. Doing a dump of its type:

diff --git a/app/Models/ThreadModel.php b/app/Models/ThreadModel.php
index fac36b3..74d0dfc 100644
--- a/app/Models/ThreadModel.php
+++ b/app/Models/ThreadModel.php
@@ -172,6 +172,8 @@ class ThreadModel extends Model

         $categoryModel = model(CategoryModel::class);

+        \PHPStan\dumpType($data['data']['category_id']);
+
         // Update stats for new category
         $newCategory = $categoryModel->allowCallbacks(false)->find($data['data']['category_id']);
         $newCategory->thread_count++;

will give :175 Dumped type: mixed

It needs to know that the category_id index is either string or null. You can solve this by setting up an array shape to the parameter $data. Or do an assert call on category_id to make sure it is int|string.

@paulbalandan
Copy link
Collaborator

Assuming you did an assert call instead to is_int.

diff --git a/app/Models/ThreadModel.php b/app/Models/ThreadModel.php
index fac36b3..e5b8711 100644
--- a/app/Models/ThreadModel.php
+++ b/app/Models/ThreadModel.php
@@ -171,6 +171,8 @@ class ThreadModel extends Model
         }

         $categoryModel = model(CategoryModel::class);
+        
+        assert(is_int($data['data']['category_id']));

         // Update stats for new category
         $newCategory = $categoryModel->allowCallbacks(false)->find($data['data']['category_id']);

The errors now would be:

------ -----------------------------------------------------------
  Line   app\Models\ThreadModel.php
 ------ -----------------------------------------------------------
  :179   Cannot use ++ on array|bool|float|int|object|string|null.
  :197   Cannot use -- on array|bool|float|int|object|string|null.
 ------ -----------------------------------------------------------

At the moment, this extension does not yet understand the casts of Entities.

@kenjis
Copy link
Author

kenjis commented Nov 9, 2023

Why does it assume array<int, App\Entities\Category> when $id is mixed?

@paulbalandan
Copy link
Collaborator

Because of these lines:

if ($idType->isNull()->yes()) {
return $this->getTypeFromFindAll($methodReflection, $methodCall, $scope);
}
if ($idType->isInteger()->yes() || $idType->isString()->yes()) {
$classReflection = $this->getClassReflection($methodCall, $scope);
return TypeCombinator::addNull($this->modelFetchedReturnTypeHelper->getFetchedReturnType($classReflection, $methodCall, $scope));
}
return $this->getTypeFromFindAll($methodReflection, $methodCall, $scope);

If the type is mixed, and since the parameter type of $id is array|int|string|null, the executed line will be the last line because the type will be simplified to array|null.

@kenjis
Copy link
Author

kenjis commented Nov 9, 2023

At the moment, this extension does not yet understand the casts of Entities.

Why the casts of Entities that can be understood without specifying a type cannot be understood with specifying type with assert()?

@paulbalandan
Copy link
Collaborator

By default, PHPStan does not know what is the relation of $casts to the properties of Entities. We need to teach it that the content of $casts influence the type of the properties.

@kenjis
Copy link
Author

kenjis commented Nov 9, 2023

Ah, by default PHPStan does not know the Entiy property types. I misunderstood it.

@kenjis
Copy link
Author

kenjis commented Nov 9, 2023

The logic below is not correct after all in many cases. Because guessed mixed does not means it is really array.
This case codeigniter4projects/playground#218 (comment) is also an incorrect error.

if ($idType->isNull()->yes()) {
return $this->getTypeFromFindAll($methodReflection, $methodCall, $scope);
}
if ($idType->isInteger()->yes() || $idType->isString()->yes()) {
$classReflection = $this->getClassReflection($methodCall, $scope);
return TypeCombinator::addNull($this->modelFetchedReturnTypeHelper->getFetchedReturnType($classReflection, $methodCall, $scope));
}
return $this->getTypeFromFindAll($methodReflection, $methodCall, $scope);

The last line should be the second return, because finding single record is the most use cases for find().
And if $id is surely an array, it should return the last return.

@paulbalandan
Copy link
Collaborator

If we'll make the last line as an if check to array, then we still need to account for all other cases.

Like, if passed id is not array|int|string|null does the framework throw an error so should the type be never?

@kenjis
Copy link
Author

kenjis commented Nov 9, 2023

I mean like this.

            function (Type $idType, callable $traverse) use ($methodReflection, $methodCall, $scope): Type {
                if ($idType instanceof UnionType || $idType instanceof IntersectionType) {
                    return $traverse($idType);
                }

                if ($idType->isNull()->yes() || $idType->isArray()->yes()) {
                    return $this->getTypeFromFindAll($methodReflection, $methodCall, $scope);
                }

                if ($idType->isInteger()->yes() || $idType->isString()->yes()) {
                    $classReflection = $this->getClassReflection($methodCall, $scope);

                    return TypeCombinator::addNull($this->modelFetchedReturnTypeHelper->getFetchedReturnType($classReflection, $methodCall, $scope));
                }

                // We don't know the correct type, we assume it is a int or string.
                $classReflection = $this->getClassReflection($methodCall, $scope);

                return TypeCombinator::addNull($this->modelFetchedReturnTypeHelper->getFetchedReturnType($classReflection, $methodCall, $scope));
            }

@paulbalandan
Copy link
Collaborator

I think the current code is correct. Looking at the code of Model::doFind, the check is first see if id is array, or check if string or int, then it has an else encompassing all other cases which returns all records.

@kenjis
Copy link
Author

kenjis commented Nov 10, 2023

See the product code. $newCategory is a single record, not an array.

@paulbalandan
Copy link
Collaborator

It's because at runtime the id resolves to either a string or null. However, phpstan does not run the code. Thus, it thinks that the id is mixed.

@kenjis
Copy link
Author

kenjis commented Nov 10, 2023

Yes, but the mixed is incorrect, or at least thinking $newCategory is an array is wrong.
Aside from what the internal process is doing, this extension often reports false positive errors on find().

@MGatner
Copy link
Member

MGatner commented Nov 18, 2023

I might be stating something stupidly obvious and not helpful but... wouldn't a conditional return on find() be enough to differentiate the two completely different uses?

https://phpstan.org/blog/phpstan-1-6-0-with-conditional-return-types

@paulbalandan
Copy link
Collaborator

It wouldn't be easy. You would need to annotate that the each calling model returns a return type (array, Entity, custom object, stdClass). Maybe generics could do the job? Also, you need to note that when asArray() and asObject() is called the return type also changes.

@kenjis
Copy link
Author

kenjis commented Nov 19, 2023

I already sent codeigniter4/CodeIgniter4#8187

@MGatner
Copy link
Member

MGatner commented Nov 19, 2023

Thanks you two. I think all of the work going into this is telling what a mess Model/BaseModel is. 😓

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants