skip to Main Content

Background

I do have classes (here: ClassA and ClassB) that shall be tested. As these classes are similar, I created an abstract test-class that implements an interface.

Problem

At level 7, PhpStan is unhappy and reports these issues:

  • Call to an undefined method object::getId()
  • Call to an undefined method object::getName()

I do know that it is unclear which object is calling the methods and thus the issue is reported and I know that the solution is by using GENERICS.

Question

I just started with the use of generics, I have no clue how to solve this correctly.
Anybody so kind to support me here? Thanks!

Code

see PhpStan Playground.

REMARK: The example here is simplified to support the discussion.

<?php

// ---------------------------------------- CLASSES
class ClassA {
    public function getId(): int {
        return 123;
    }
}
class ClassB {
    public function getName(): string {
        return 'abc';
    }
}

// ---------------------------------------- TESTS

/**
 * @template T
 */
interface InterfaceForTest {
    public function getClassName(): string; 
}

/**
 * @template T
 * @implements InterfaceForTest<T>
 */
abstract class AbstractClassTest implements InterfaceForTest {
    public function createObject(): object
    {
        $class  = $this->getClassName();
        $object = new $class();
        return $object;
    }
}

/**
 * @template T
 * @extends AbstractClassTest<T>
 */
class ClassATest extends AbstractClassTest {
    public function getClassName(): string
    {
        return ClassA::class;
    }

    public function testA(): void
    {
        $obj = $this->createObject();

        assert($obj->getId() === 123);      // makes PHPStan unhappy at level 7
    }
}

/**
 * @template T
 * @extends AbstractClassTest<T>
 */
class ClassBTest extends AbstractClassTest {
    public function getClassName(): string
    {
        return ClassB::class;
    }

    public function testB(): void
    {
        $obj = $this->createObject();
        
        assert($obj->getName() === 'abc');  // makes PHPStan unhappy at level 7
    }
}

2

Answers


  1. Chosen as BEST ANSWER

    Solution

    I found the solution. See below

    <?php
    
    class ClassA {
        public function getId(): int {
            return 123;
        }
    }
    
    class ClassB {
        public function getName(): string {
            return 'abc';
        }
    }
    
    interface InterfaceForTest {
        public function getClassName(): string;
    }
    
    /**
     * @template T
     */
    abstract class AbstractClassTest implements InterfaceForTest {
        /**
         * @return class-string<T>
         */
        abstract public function getClassName(): string;
    
        /**
         * @return T
         */
        public function createObject() {
            $class = $this->getClassName();
            return new $class();
        }
    }
    
    /**
     * @extends AbstractClassTest<ClassA>
     */
    class ClassATest extends AbstractClassTest {
        /**
         * @return class-string<ClassA>
         */
        public function getClassName(): string {
            return ClassA::class;
        }
    
        public function testA(): void {
            $obj = $this->createObject();
            assert($obj->getId() === 123);
        }
    }
    
    /**
     * @extends AbstractClassTest<ClassB>
     */
    class ClassBTest extends AbstractClassTest {
        /**
         * @return class-string<ClassB>
         */
        public function getClassName(): string {
            return ClassB::class;
        }
    
        public function testB(): void {
            $obj = $this->createObject();
            assert($obj->getName() === 'abc');
        }
    }
    
    

  2. You need to use generics more effectively by specifying the type of object that your abstract class will create and manipulate. This will involve using PHPDoc annotations to inform PHPStan about the types it can expect at runtime. To address PHPStan’s type-checking issues at level 7 in the given PHP code, I utilized PHPDoc annotations to clarify the types returned by methods in an abstract generic class system. Specifically, I refined the AbstractClassTest and its subclasses (ClassATest and ClassBTest) using @extends and @method annotations. These annotations explicitly define the specific class types (ClassA or ClassB) that each subclass works with, and ensure that the createObject() method in each subclass is understood to return instances of these specific classes. This setup guides PHPStan to correctly infer the available methods (getId() and getName()) on objects returned by createObject(), thereby preventing it from flagging these method calls as errors.

    Try this –>

    <?php
    
    class ClassA {
        public function getId(): int {
            return 123;
        }
    }
    
    class ClassB {
        public function getName(): string {
            return 'abc';
        }
    }
    
    /**
     * @template T
     */
    interface InterfaceForTest {
        public function getClassName(): string;
    }
    
    /**
     * @template T
     * @implements InterfaceForTest<T>
     */
    abstract class AbstractClassTest implements InterfaceForTest {
        /**
         * Returns an instance of type T.
         * @return T
         */
        public function createObject() {
            $class = $this->getClassName();
            return new $class();
        }
    }
    
    /**
     * @extends AbstractClassTest<ClassA>
     */
    class ClassATest extends AbstractClassTest {
        /**
         * Specifies the class name for the generic instantiation.
         * @return string The class name of ClassA.
         */
        public function getClassName(): string {
            return ClassA::class;
        }
    
        /**
         * Test function for ClassA to assert the ID.
         */
        public function testA(): void {
            $obj = $this->createObject();
            assert($obj->getId() === 123);
        }
    }
    
    /**
     * @extends AbstractClassTest<ClassB>
     */
    class ClassBTest extends AbstractClassTest {
        /**
         * Specifies the class name for the generic instantiation.
         * @return string The class name of ClassB.
         */
        public function getClassName(): string {
            return ClassB::class;
        }
    
        /**
         * Test function for ClassB to assert the Name.
         */
        public function testB(): void {
            $obj = $this->createObject();
            assert($obj->getName() === 'abc');
        }
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search