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

Large refactor and new features #6

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from

Conversation

justinbeaty
Copy link

I created a new PHPStan plugin, and I thought it would be nice for OM and Maho to be on the same page with regards to fixing errors. This is a rather large PR... I originally tried up using the Maho plugin on OM, but it doesn't work due to the differences in autoloaders, etc. I am making this PR with absolutely no licensing restrictions, so feel free to adapt as you see fit if the PR is too large or different than how things currently are.

With this plugin, on OM 20.0 branch I am seeing:
-76 false positive errors gone from the baseline
+589 new errors

There are three main areas of improvement:

Varien Object Handling

Currently, magic methods always returned mixed even when there is a @method annotation:

// In Mage_Customer_Model_Customer:
//  * @method string getFirstname()

$model = Mage::getModel('customer/customer');
$firstname = $model->getFirstname();

\PHPStan\dumpType($firstname);
# Old: mixed
# New: string

Due to the magic methods always returning mixed, PHPStan doesn't detect certain errors:

$model = Mage::getModel('customer/customer');
$firstname = $model->getFirstname();
$firstname->foobar();

# Old: [OK] No errors
# New: Cannot call method foobar() on string.

Magic methods had no argument validation:

$model = Mage::getModel('customer/customer');
$firstname = $model->getFirstname(true, false, 'foobar');

# Old: [OK] No errors
# New: Method Mage_Customer_Model_Customer::getFirstname() invoked with 3 parameters, 0 required.

Without a @method annotation:

$model = Mage::getModel('customer/customer');
$model->setFoobar('bar', 'baz');

# Old: [OK] No errors
# New: Method Varien_Object::setData() invoked with 2 parameters, 0-1 required.

There is also a new enforceMagicMethodDocBlock option that can be turned on eventually, requiring all classes that extend Varien_Object to define @method annotations.

Return Type Detector

Return type detector only supported literal strings as the argument:

$module = 'catalog';
$helper = Mage::helper($module);

\PHPStan\dumpType($helper);
# Old: Dumped type: Mage_Core_Helper_Abstract
# New: Dumped type: Mage_Catalog_Helper_Data

New code even detects multiple possibilities:

$module = rand() % 2 ? 'catalog' : 'core';
$helper = Mage::helper($module);

\PHPStan\dumpType($helper);
# Old: Dumped type: Mage_Core_Helper_Abstract
# New: Dumped type: Mage_Catalog_Helper_Data|Mage_Core_Helper_Data

Better invalid class detection:

$model = Mage::getModel('foo');

\PHPStan\dumpType($model);
# Old: Dumped type: foo
# New: Dumped type: false - Call to Mage::getModel('foo') resulted in invalid type foo.

Support for getResourceSingleton:

$model = Mage::getResourceSingleton('catalog/category');

\PHPStan\dumpType($model);
# Old: Dumped type: object
# New: Dumped type: Mage_Catalog_Model_Resource_Category

Template File $this Reflection:

Even though we use @var ... $this annotations in .phtml files, before we would get an error if we try to call protected/private methods or properties. Not many block classes have protected methods, but some do...

/** @var Mage_Bundle_Block_Catalog_Product_View_Type_Bundle_Option $this */
$this->_getDefaultValues();

# Old: Call to protected method _getDefaultValues() of class Mage_Bundle_Block_Catalog_Product_View_Type_Bundle_Option
# New: [OK] No errors

@justinbeaty
Copy link
Author

Ping @sreichel, let me know what you think.

@justinbeaty justinbeaty marked this pull request as draft December 8, 2024 19:14
@justinbeaty
Copy link
Author

Actually, I have an idea that might make this all easier. Will test soon...

@sreichel
Copy link

sreichel commented Dec 9, 2024

@justinbeaty man, you r crazy 🚀

I'll test as fast as possbile.

G R E A T 😄

@sreichel
Copy link

sreichel commented Dec 9, 2024

Testing with OpenMage/magento-lts#4405 ... and looks rly good. 😄

@sreichel
Copy link

sreichel commented Dec 9, 2024

Found a bug ...

 ------ ----------------------------------------------------------------- 
  Line   app/code/core/Mage/Catalog/Model/Category/Indexer/Product.php    
 ------ ----------------------------------------------------------------- 
  116    Call to an undefined method Varien_Object::dataHasChangedFor().  
         🪪  method.notFound                                               
  116    Call to an undefined method Varien_Object::isObjectNew().        
         🪪  method.notFound                                               
  123    Call to an undefined method Varien_Object::dataHasChangedFor().  
         🪪  method.notFound                                               
  124    Call to an undefined method Varien_Object::dataHasChangedFor().  
         🪪  method.notFound                                               
  125    Call to an undefined method Varien_Object::isObjectNew().        
         🪪  method.notFound  

Thes methods belog to ...

abstract class Mage_Core_Model_Abstract extends Varien_Object

@justinbeaty
Copy link
Author

justinbeaty commented Dec 9, 2024

@sreichel It's not a bug.

$store = $event->getDataObject();
\PHPStan\dumpType($store);

# Before: Dumped type: mixed
# After: Dumped type: Varien_Object

Because previously the plugin didn't read @method annotations and just returned mixed , which PHPStan will not complain about calling any method on:

/**
 * @method Varien_Object getDataObject()
 */
class Mage_Index_Model_Event extends Mage_Core_Model_Abstract
{
// ...

This fixes it:

        $entity = $event->getEntity();
        if ($entity == Mage_Core_Model_Store::ENTITY) {
            /** @var Mage_Core_Model_Store */
            $store = $event->getDataObject();
            // ...
        } elseif ($entity == Mage_Core_Model_Store_Group::ENTITY) {
            /** @var Mage_Core_Model_Store_Group */
            $storeGroup = $event->getDataObject();
            // ...
        }

Edit: it also works to do this as long as we only use methods defined on both types:

/**
 * @method Mage_Core_Model_Store|Mage_Core_Model_Store_Group getDataObject()
 */
class Mage_Index_Model_Event extends Mage_Core_Model_Abstract
{
// ...

Some other ideas to solve things you reported in OpenMage/magento-lts#687 "How to deal with observers?"

  1. No extra class is needed. We could define a new type where the observer is fired:
 /** @phpstan-type MyEventType Varien_Object<loaded: bool, forward_module: string, ...> */

And then import it in the observer with:

/** @phpstan-import-type MyEventType from Mage_Core_Whatever_Class_Defined_It */

But it needs to support custom Object Shapes for Varien_Object class.

I also had the idea to do something like /** @var Varien_Object<Mage_Customer_Model_Customer> */ which would create a new type with all magic methods defined on it. So you could do:

if ($someCondition) {
    $customer = Mage::getModel('customer/customer');
} else {
    /** @var Varien_Object<Mage_Customer_Model_Customer> */
    $customer = new Varien_Object();
}

$customer->setFirstname('foo');
$customer->getFirstname(); // string instead of mixed

@sreichel
Copy link

sreichel commented Dec 9, 2024

You are right. Not a bug - its resolved correct.

Ummm. Nice.

But it needs to support custom Object Shapes for Varien_Object class.

I think "custom" is the problem ....

I tried with stubs, but stubs for code you control ... meh.

The best solution ...imho ... would be to add pseudo classes that extend Varien_Object. Every observer passes its own class ... not only VO.

@justinbeaty
Copy link
Author

I think "custom" is the problem ....

It is possible using Custom PHPDoc Type Extension, this is how I supported $this in phtml files. It's not too much work to accomplish, just haven't done it yet.

@sreichel
Copy link

sreichel commented Dec 9, 2024

What does takes more effort? Adding ObjectShapes or add classes - with real methods?

In both cases you have to define the methods. Using real methods take more code, but see,s most clean to me.

@justinbeaty
Copy link
Author

justinbeaty commented Dec 9, 2024

I think object shapes are better because they're more versatile.

With real classes, how do you solve this?

if ($someCondition) {
    $customer = Mage::getModel('customer/customer');
} else {
    // $customer = new Varien_Object();
    $customer = new Mage_Customer_MockModel_Customer() // ??
}

You can't have Mage_Customer_MockModel_Customer extend Mage_Customer_Model_Customer, because the mock object doesn't have methods like authenticate.

So, you'd have to keep both classes in sync with the same the @method annotations / method signatures for getters. It gets even worse if the class extends several others before getting to Varien_Object, then you have to copy/paste all getter methods in the chain.


Also, what about this?

function doFoo($obj) {
    $obj->getSomeValue(); // what is the type?
}

$foo = new Varien_Object();
$foo->setSomeValue('bar');
doFoo($foo);

Do you create a new class Mage_Core_MockModel_MyFoo ?

The idea with object shape is that if you say /** @var Varien_Object<some_value: string> */ then you cannot call $foo->getBar() because it doesn't exist. At least when the enforceMagicMethodDocBlock option is enabled.

@sreichel
Copy link

sreichel commented Dec 9, 2024

if ($someCondition) {
    $customer = Mage::getModel('customer/customer');
} else {
    // $customer = new Varien_Object();
    $customer = new Mage_Customer_MockModel_Customer() // ??
}

Theoreticaly ... shouldnt that be something like public function getCustomer(): Mage_Customer_Model_Customer and have authenticate included in some other way? (instanceof, method_exists, ...)


Do you create a new class Mage_Core_MockModel_MyFoo ?

For observer, yes. (?) But that may require two classes ...

  • 1st to extend Varien_Observer
  • 2nd to extend Varien_Observer_Event (in some cases)

@justinbeaty
Copy link
Author

justinbeaty commented Dec 9, 2024

Theoreticaly ... shouldnt that be something like public function getCustomer(): Mage_Customer_Model_Customer and have authenticate included in some other way? (instanceof, method_exists, ...)

You'd have to have this:

function getCustomer(): Mage_Customer_Model_Customer|Mage_Customer_MockModel_Customer
{
    if ($someCondition) {
        $customer = Mage::getModel('customer/customer');
    } else {
        $customer = new Mage_Customer_MockModel_Customer();
    }
}

But to me it's just cleaner to have:

function getCustomer(): Mage_Customer_Model_Customer|Varien_Object<Mage_Customer_Model_Customer>
{
    if ($someCondition) {
        $customer = Mage::getModel('customer/customer');
    } else {
        /** @var Varien_Object<Mage_Customer_Model_Customer> */
        $customer = new Varien_Object();
    }
}

And then PHPStan knows that if you call getFirstname() it's a string.

@sreichel
Copy link

sreichel commented Dec 9, 2024

Its cleaner, but i dont know if it saves much lines of code.

Btw ... Php7 has no union types :(

(one reason for me to drop php7-support)

@justinbeaty
Copy link
Author

Its cleaner, but i dont know if it saves much lines of code.

It does though, because you don't need to create (hundreds?) of new classes to support every single event. I feel like real classes would also overcomplicate an already obscure event system in Magento.

Btw ... Php7 has no union types :(

Doesn't PHPStan still support it in @var tags?

@justinbeaty
Copy link
Author

Btw, see here: OpenMage/magento-lts#4411

In the Maho version of the plugin I already removed the whole define('staticReflection', true); thing, because IMO it's really pointless for a while in PHPStan. It always just creates a real Mage_Core_Model_Config object.

@tmotyl
Copy link
Member

tmotyl commented Dec 11, 2024

Hi guys
If its helpful I'm open to transfer the repo owenership to openmage namespace/organization.

@sreichel
Copy link

First we should finish this :)

As long you have time to maintain it, it should stay here ... imho.

@justinbeaty
Copy link
Author

The code can definitely stay here, just didn't know if it was too big of a change.

The main difference between this PR, and the one in OpenMage/magento-lts#4411 is that I removed the two different "modes", i.e. extension.neon vs extension-mage-autoload.neon as I think the separation is unnecessary.

This gets rid of the PHPStanMagento1\Config\MagentoCore class entirely, and instead uses the actual Mage_Core_Model_Config class in the codebase. Otherwise, all of the changes in 4411 need to be migrated back, and I don't see a huge value in having them duplicated, and it could lead to hidden bugs.

This means that running PHPStan can connect to the database if local.xml is present, but that only causes errors if the database doesn't exist. But in any case, this is why I added a config option useLocalXml that is by default set to false. However that required changes to app/Mage.php present in 4411 since there was no other way to tell Magento to not load local.xml.

I also removed all of the autoloader stuff from the plugin in 4411 as PHPStan doesn't need an autoloader to work, it just scans the source files to find the appropriate classes. However we do need an autoloader to load and run Mage_Core_Model_Config. The easiest way to do that is to just include app/Mage.php, which is done as a bootstrap file.

4411 also adds in support for some other dynamic return types, but that also required adding in some new methods to Mage_Core_Model_Config.

This is why I said that the new plugin wouldn't exactly be compatible with non-OM codebases, but the plugin could check the existence of some of these methods to not break backwards compatibility.

I can update this PR with the newest code, but before I do that you can browse the new plugin here: https://github.com/justinbeaty/maho-phpstan-plugin/tree/topic-v3

@tmotyl
Copy link
Member

tmotyl commented Dec 18, 2024

@justinbeaty I'm ok with bigger refactorings and removing options which doesn't make sense now.

@sreichel
Copy link

@justinbeaty i'm testing it on latest OM w/o 4411 ...

Internal error: Call to undefined method Mage_Core_Model_Config::getResourceHelperClassName() while analysing file /var/www/html/app/code/core/Mage/Sales/Model/Resource/Helper/Mysql4.php  
                                                                                                                                                                                         
Run PHPStan with -v option and post the stack trace to:                                                                                                                                     
https://github.com/phpstan/phpstan/issues/new?template=Bug_report.yaml                                                                                                                      
                                                                                                                                                                                         
Child process error (exit code 255): <pre>                                                                                                                                                  
Fatal error: Cannot declare class Varien_Date, because the name is already in use in /var/www/html/lib/Varien/Date.php on line 24                                                           
</pre>PHP Notice:  Constant DS already defined in /var/www/html/app/Mage.php on line 17                                                                                                     
Notice: Constant DS already defined in /var/www/html/app/Mage.php on line 17                                                                                                                
PHP Notice:  Constant PS already defined in /var/www/html/app/Mage.php on line 18                                                                                                           
Notice: Constant PS already defined in /var/www/html/app/Mage.php on line 18                                                                                                                
PHP Fatal error:  Cannot declare class Varien_Date, because the name is already in use in /var/www/html/lib/Varien/Date.php on line 24                                                      
while running parallel worker                                                                                                                                                              
Child process error (exit code 255): <pre>                                                                                                                                                  
Fatal error: Cannot declare class Varien_Image, because the name is already in use in /var/www/html/lib/Varien/Image.php on line 23                                                         
</pre>PHP Notice:  Constant DS already defined in /var/www/html/app/Mage.php on line 17                                                                                                     
Notice: Constant DS already defined in /var/www/html/app/Mage.php on line 17                                                                                                                
PHP Notice:  Constant PS already defined in /var/www/html/app/Mage.php on line 18                                                                                                           
Notice: Constant PS already defined in /var/www/html/app/Mage.php on line 18                                                                                                                
PHP Fatal error:  Cannot declare class Varien_Image, because the name is already in use in /var/www/html/lib/Varien/Image.php on line 23                                                    
while running parallel worker       

@justinbeaty
Copy link
Author

@justinbeaty i'm testing it on latest OM w/o 4411 ...

You must use 4411, or we can try to add:

        case 'Mage::getResourceHelper':
        case 'Mage_Core_Model_Config::getResourceHelper':
        case 'Mage_Core_Model_Config::getResourceHelperInstance':
            return method_exists($this->getConfig(), 'getResourceHelperClassName')
                ? fn (string $alias) => $this->getConfig()->getResourceHelperClassName($alias)
                : null;

@justinbeaty
Copy link
Author

Pushed the change, not tested yet though.

I'm trying to figure out the Varien_Date thing though...

@justinbeaty
Copy link
Author

Idk what the Varien_Date error is. Maybe it's fixed with phpstan v2?

@sreichel
Copy link

It worked before your todays changes. I was just about to compare with topic-v3.

@sreichel
Copy link

Fatal error: Cannot declare class Varien_Image, because the name is already in use in /var/www/html/lib/Varien/Image.php on line 23

On comes from ... Mage_ConfigurableSwatches_Helper_Mediafallback

Last lines from call stack ...

     PHP  50. PHPStan\Reflection\BetterReflection\SourceLocator\FileReadTrapStreamWrapper::withStreamWrapperOverride($executeMeWithinStreamWrapperOverride = class Closure { virtual $closure =                                                                                                                                
     "PHPStan\Reflection\BetterReflection\SourceLocator\AutoloadSourceLocator::PHPStan\Reflection\BetterReflection\SourceLocator\{closure}", public $static = ['className' => 'image'] }, $streamWrapperProtocols =                                                                                                            
     *uninitialized*) phar:///var/www/html/vendor/phpstan/phpstan/phpstan.phar/src/Reflection/BetterReflection/SourceLocator/AutoloadSourceLocator.php:278                                                                                                                                                                     
     PHP  51.                                                                                                                                                                                                                                                                                                                  
     PHPStan\Reflection\BetterReflection\SourceLocator\AutoloadSourceLocator::PHPStan\Reflection\BetterReflection\SourceLocator\{closure:phar:///var/www/html/vendor/phpstan/phpstan/phpstan.phar/src/Reflection/BetterReflection/SourceLocator/AutoloadSourceLocator.php:260-278}()                                           
     phar:///var/www/html/vendor/phpstan/phpstan/phpstan.phar/src/Reflection/BetterReflection/SourceLocator/FileReadTrapStreamWrapper.php:56                                                                                                                                                                                   
     PHP  52. Varien_Autoload->autoload($class = 'image') phar:///var/www/html/vendor/phpstan/phpstan/phpstan.phar/src/Reflection/BetterReflection/SourceLocator/AutoloadSourceLocator.php:266                                                                                                                                 
      while running parallel worker   

@sreichel
Copy link

sreichel commented Dec 26, 2024

Fatal error: Cannot declare class Varien_Date, because the name is already in use in /var/www/html/lib/Varien/Date.php on line 24

Similiar stacktrace ...

     PHP  44.                                                                                                                                                                                                                                                                                                                                   
     PHPStan\Reflection\BetterReflection\SourceLocator\AutoloadSourceLocator::PHPStan\Reflection\BetterReflection\SourceLocator\{closure:phar:///var/www/html/vendor/phpstan/phpstan/phpstan.phar/src/Reflection/BetterReflection/SourceLocator/AutoloadSourceLocator.php:260-278}()                                                            
     phar:///var/www/html/vendor/phpstan/phpstan/phpstan.phar/src/Reflection/BetterReflection/SourceLocator/FileReadTrapStreamWrapper.php:56                                                                                                                                                                                                    
     PHP  45. Varien_Autoload->autoload($class = 'date') phar:///var/www/html/vendor/phpstan/phpstan/phpstan.phar/src/Reflection/BetterReflection/SourceLocator/AutoloadSourceLocator.php:266                                                                                                                                                   
      while running parallel worker                                                                                                                                                                                                                                                                                                             

... but the file is Mage_Eav_Model_Adminhtml_System_Config_Source_Inputtype with that content:

    public function toOptionArray()
    {
        return [
            ['value' => 'text', 'label' => Mage::helper('eav')->__('Text Field')],
            ['value' => 'textarea', 'label' => Mage::helper('eav')->__('Text Area')],
            ['value' => 'date', 'label' => Mage::helper('eav')->__('Date')],
            ['value' => 'boolean', 'label' => Mage::helper('eav')->__('Yes/No')],
            ['value' => 'multiselect', 'label' => Mage::helper('eav')->__('Multiple Select')],
            ['value' => 'select', 'label' => Mage::helper('eav')->__('Dropdown')],
        ];
    }

Somehow the error comes from ['value' => 'date',. Change "date" to something else let the error disappear.

Name it "image", "crypt" or like another Varien_ class and error is back.

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.

3 participants