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

Mock readonly classes on PHP 82 #1319

Draft
wants to merge 26 commits into
base: 1.6.x
Choose a base branch
from

Conversation

ghostwriter
Copy link
Member

@ghostwriter ghostwriter commented Aug 3, 2023

readonly Class Semantics

When a class is declared readonly, the entirety of the class and its declared members are considered readonly. https://php.watch/versions/8.2/readonly-classes#semantics

A readonly property:

  • Only be initialized from within the class scope
  • Cannot be modified once initialized
  • Cannot be unset once initialized
  • Must be a typed property

When a class is declared readonly, that class:

  • Must only contain typed properties
  • Must not use dynamic properties
  • Must not use #[\AllowDynamicProperties] attribute
  • Cannot opt-out of the read-only status

When a readonly class is extended by a subclass, that class:

  • Must also declare readonly
  • Cannot opt-out of readonly

Resolve #1317


TODO: We need to prioritize extracting mock internals object first, because currently Mock class has many dynamic properties. (currently, string replacement is not enough to generate mocks for readonly classes)

@ghostwriter ghostwriter added Patch Backwards compatible bug fixes and improvements Fixed for bug fixes or error corrections labels Aug 3, 2023
@ghostwriter ghostwriter added this to the 1.6.5 milestone Aug 3, 2023
@ghostwriter ghostwriter self-assigned this Aug 3, 2023
@ghostwriter ghostwriter linked an issue Aug 3, 2023 that may be closed by this pull request
@ghostwriter ghostwriter marked this pull request as draft August 3, 2023 22:21
@codecov
Copy link

codecov bot commented Aug 3, 2023

Codecov Report

Merging #1319 (f0f8052) into 1.6.x (e054424) will decrease coverage by 0.20%.
The diff coverage is 38.88%.

❗ Current head f0f8052 differs from pull request most recent head a0a71d3. Consider uploading reports for the commit a0a71d3 to get more accurate results

Additional details and impacted files
@@             Coverage Diff              @@
##              1.6.x    #1319      +/-   ##
============================================
- Coverage     77.76%   77.57%   -0.20%     
+ Complexity     1021     1019       -2     
============================================
  Files            76       76              
  Lines          2613     2613              
============================================
- Hits           2032     2027       -5     
- Misses          581      586       +5     
Files Coverage Δ
library/Mockery/Generator/UndefinedTargetClass.php 60.60% <100.00%> (+2.54%) ⬆️
library/Mockery/Generator/DefinedTargetClass.php 82.45% <75.00%> (-0.57%) ⬇️
...enerator/StringManipulation/Pass/ClassNamePass.php 62.96% <16.66%> (-37.04%) ⬇️

... and 7 files with indirect coverage changes

@ghostwriter
Copy link
Member Author

  • readonly classes must only contain typed properties

PHP Fatal error: Readonly property Mockery_191_AbstractReadOnlyClass::$_mockery_expectations must have type

@ghostwriter ghostwriter modified the milestones: 1.6.5, 1.7.0 Aug 4, 2023
@ghostwriter ghostwriter force-pushed the feature/php82/mock-readonly-classes branch from 043aaf0 to 3264adb Compare August 5, 2023 03:47
@ghostwriter ghostwriter added Major Backwards Incompatible changes and removed Patch Backwards compatible bug fixes and improvements labels Aug 9, 2023
@ghostwriter ghostwriter modified the milestones: 1.6.6, 2.0.0 Aug 9, 2023
Signed-off-by: Nathanael Esayeas <nathanael.esayeas@protonmail.com>
Signed-off-by: Nathanael Esayeas <nathanael.esayeas@protonmail.com>
Signed-off-by: Nathanael Esayeas <nathanael.esayeas@protonmail.com>
Signed-off-by: Nathanael Esayeas <nathanael.esayeas@protonmail.com>
Signed-off-by: Nathanael Esayeas <nathanael.esayeas@protonmail.com>
Signed-off-by: Nathanael Esayeas <nathanael.esayeas@protonmail.com>
Signed-off-by: Nathanael Esayeas <nathanael.esayeas@protonmail.com>
Signed-off-by: Nathanael Esayeas <nathanael.esayeas@protonmail.com>
Signed-off-by: Nathanael Esayeas <nathanael.esayeas@protonmail.com>
Signed-off-by: Nathanael Esayeas <nathanael.esayeas@protonmail.com>
Signed-off-by: Nathanael Esayeas <nathanael.esayeas@protonmail.com>
Signed-off-by: Nathanael Esayeas <nathanael.esayeas@protonmail.com>
Signed-off-by: Nathanael Esayeas <nathanael.esayeas@protonmail.com>
Signed-off-by: Nathanael Esayeas <nathanael.esayeas@protonmail.com>
Signed-off-by: Nathanael Esayeas <nathanael.esayeas@protonmail.com>
Signed-off-by: Nathanael Esayeas <nathanael.esayeas@protonmail.com>
Signed-off-by: Nathanael Esayeas <nathanael.esayeas@protonmail.com>
Signed-off-by: Nathanael Esayeas <nathanael.esayeas@protonmail.com>
Signed-off-by: Nathanael Esayeas <nathanael.esayeas@protonmail.com>
Signed-off-by: Nathanael Esayeas <nathanael.esayeas@protonmail.com>
Signed-off-by: Nathanael Esayeas <nathanael.esayeas@protonmail.com>
Signed-off-by: Nathanael Esayeas <nathanael.esayeas@protonmail.com>
Signed-off-by: Nathanael Esayeas <nathanael.esayeas@protonmail.com>
Signed-off-by: Nathanael Esayeas <nathanael.esayeas@protonmail.com>
Signed-off-by: Nathanael Esayeas <nathanael.esayeas@protonmail.com>
Signed-off-by: Nathanael Esayeas <nathanael.esayeas@protonmail.com>
@ghostwriter ghostwriter force-pushed the feature/php82/mock-readonly-classes branch from f0f8052 to a0a71d3 Compare November 17, 2023 22:55
@comes
Copy link

comes commented Feb 21, 2024

is there any update mocking readonly classes?

@ghostwriter
Copy link
Member Author

@comes No, not yet.

However, I do have a solution in mind.

I believe we may need to move all of the class properties in the Mock class into a separate class MockInternals,

to get around the requires typed properties & no dynamic properties restrictions,

and manipulate the mock and any calls to the mock using static calls to MockInternals.

I'll try make some time to test the solution this weekend.

@ghostwriter
Copy link
Member Author

Update: refactored the MockInternals as previously mentioned, (without) changing any of the current functionality.

Encountered issues with at least two features that are now broken.

  • Mocking a __construct method (only when explicitly defining expect(‘__construct'))
  • Mocking something non-existent mock() (with no args)

These issues will require some debugging to resolve.

For readonly:

  • Added a new ReadOnlyPass instance of Pass
    • Removes \AllowDynamicProperties attribute
    • Declare readonly
    • Add types to properties
Mock & MockInternals Class

Mock

// …
class Mock implements MockInterface
{
    /**
     * @var MockInternals|null
     */
    private static $mockInternals;

    private static function mockInternals(): MockInternals
    {
        if (self::$mockInternals instanceof MockInternals){
            return self::$mockInternals;
        }

        return self::$mockInternals = MockInternals::new(__CLASS__);
    }
}
// ...

MockInternals

<?php

declare(strict_types=1);

namespace Mockery;

use Mockery;
use Mockery\Exception\BadMethodCallException;
use Mockery\Exception\InvalidArgumentException;
use Mockery\Exception\InvalidOrderException;
use Mockery\Exception\NoMatchingExpectationException;
use ReflectionClass;
use ReflectionException;
use ReflectionMethod;
use ReflectionObject;

use const CASE_LOWER;

use function array_change_key_case;
use function array_filter;
use function array_map;
use function call_user_func;
use function call_user_func_array;
use function count;
use function get_parent_class;
use function is_callable;
use function is_string;
use function mb_stripos;
use function mb_strtolower;
use function method_exists;
use function spl_object_hash;
use function sprintf;

final class MockInternals
{
    /**
     * Order number of allocation.
     *
     * @var int
     */
    public $_mockery_allocatedOrder = 0;

    public $_mockery_allowMockingProtectedMethods = false;

    /**
     * Mock container containing this mock object.
     *
     * @var Container
     */
    public $_mockery_container = null;

    /**
     * Current ordered number.
     *
     * @var int
     */
    public $_mockery_currentOrder = 0;

    /**
     * If shouldIgnoreMissing is called, this value will be returned on all calls to missing methods.
     *
     * @var mixed
     */
    public $_mockery_defaultReturnValue = null;

    /**
     * Flag to indicate whether we can defer method calls missing from our
     * expectations.
     *
     * @var bool
     */
    public $_mockery_deferMissing = false;

    /**
     * Flag to indicate we should ignore all expectations temporarily. Used
     * mainly to prevent expectation matching when in the middle of a mock
     * object recording session.
     *
     * @var bool
     */
    public $_mockery_disableExpectationMatching = false;

    /**
     * Stores an array of all expectation directors for this mock.
     *
     * @var array<string, ExpectationDirector>
     */
    public $_mockery_expectations = [];

    /**
     * Stores an initial number of expectations that can be manipulated
     * while using the getter method.
     *
     * @var int
     */
    public $_mockery_expectations_count = 0;

    /**
     * Ordered groups.
     *
     * @var array
     */
    public $_mockery_groups = [];

    /**
     * Flag to indicate whether we can ignore method calls missing from our
     * expectations.
     *
     * @var bool
     */
    public $_mockery_ignoreMissing = false;

    /**
     * Flag to indicate whether we want to set the ignoreMissing flag on
     * mocks generated form this calls to this one.
     *
     * @var bool
     */
    public $_mockery_ignoreMissingRecursive = false;

    public $_mockery_ignoreVerification = null;

    public $_mockery_instanceMock = true;

    /**
     * @var array
     */
    public $_mockery_mockableMethods = [];

    /**
     * Stores all stubbed public methods separate from any on-object public
     * properties that may exist.
     *
     * @var array
     */
    public $_mockery_mockableProperties = [];

    /**
     * Given name of the mock.
     *
     * @var string
     */
    public $_mockery_name = null;

    /**
     * Instance of a core object on which methods are called in the event
     * it has been set, and an expectation for one of the object's methods
     * does not exist. This implements a simple partial mock proxy system.
     *
     * @var object
     */
    public $_mockery_partial = null;

    public $_mockery_receivedMethodCalls;

    /**
     * Tracks internally all the bad method call exceptions that happened during runtime.
     *
     * @var array
     */
    public $_mockery_thrownExceptions = [];

    /**
     * Flag to indicate whether this mock was verified.
     *
     * @var bool
     */
    public $_mockery_verified = false;

    /**
     * The mock object.
     *
     * @var null|Mock
     */
    public $mock;

    /**
     * Just a local cache for this mock's target's methods.
     *
     * @var ReflectionMethod[]
     */
    public static $_mockery_methods = [];

    public function __construct(string $name)
    {
        $this->_mockery_name = $name;
    }

    public function _mockery_constructorCalled(array $args): void
    {
        if (! isset($this->_mockery_expectations['__construct'])) {
            /* _mockery_handleMethodCall runs the other checks */
            return;
        }

        $this->_mockery_handleMethodCall('__construct', $args);
    }

    public function _mockery_findExpectedMethodHandler($method)
    {

        if (array_key_exists($method, $this->_mockery_expectations)) {
            return $this->_mockery_expectations[$method];
        }

        $lowerCasedMockeryExpectations = array_change_key_case($this->_mockery_expectations, CASE_LOWER);

        $lowerCasedMethod = mb_strtolower($method);

        return $lowerCasedMockeryExpectations[$lowerCasedMethod] ?? null;
    }

    public function _mockery_getReceivedMethodCalls(): ReceivedMethodCalls
    {
        if ($this->_mockery_receivedMethodCalls instanceof ReceivedMethodCalls) {
            return $this->_mockery_receivedMethodCalls;
        }

        return $this->_mockery_receivedMethodCalls = new ReceivedMethodCalls();
    }

    public function _mockery_handleMethodCall(string $method, array $args)
    {
        $this->log(__METHOD__.'|'. $method);

        $this->_mockery_getReceivedMethodCalls()
             ->push(new MethodCall($method, $args));

        $parentClass = get_parent_class($this->mock);

        $rm = $this->mockery_getMethod($method);
        if (! $this->_mockery_allowMockingProtectedMethods && $rm && $rm->isProtected()) {
            if ($rm->isAbstract()) {
                return;
            }

            try {
                $prototype = $rm->getPrototype();
                if ($prototype->isAbstract()) {
                    return;
                }
            } catch (ReflectionException $re) {
                // noop - there is no hasPrototype method
            }

            return call_user_func_array($parentClass . '::' . $method, $args);
        }

        $handler = $this->_mockery_findExpectedMethodHandler($method);

        if ($handler !== null && ! $this->_mockery_disableExpectationMatching) {
            try {
                return $handler->call($args);
            } catch (NoMatchingExpectationException $e) {
                if (! $this->_mockery_ignoreMissing && ! $this->_mockery_deferMissing) {
                    throw $e;
                }
            }
        }

        if ($this->_mockery_partial !== null &&
            (method_exists($this->_mockery_partial, $method) || method_exists($this->_mockery_partial, '__call'))) {
            return call_user_func_array([$this->_mockery_partial, $method], $args);
        }

        if ($this->_mockery_deferMissing && is_callable($parentClass . '::' . $method)
            && (! $this->hasMethodOverloadingInParentClass() || ($parentClass && method_exists($parentClass, $method)))
        ) {
            return call_user_func_array($parentClass . '::' . $method, $args);
        }

        if ($this->_mockery_deferMissing && $parentClass && method_exists($parentClass, '__call')) {
            return call_user_func($parentClass . '::__call', $method, $args);
        }

        if ($method === '__toString') {
            // __toString is special because we force its addition to the class API regardless of the
            // original implementation.  Thus, we should always return a string rather than honor
            // _mockery_ignoreMissing and break the API with an error.
            return sprintf('%s#%s', $this->_mockery_name, spl_object_hash($this->mock));
        }

        if ($this->_mockery_ignoreMissing) {
            if ( is_callable($parentClass . '::' . $method)
                || ($this->_mockery_partial !== null && method_exists($this->_mockery_partial, $method))
                || Mockery::getConfiguration()->mockingNonExistentMethodsAllowed()
            ) {
                if ($this->_mockery_defaultReturnValue instanceof Undefined) {
                    return call_user_func_array([$this->_mockery_defaultReturnValue, $method], $args);
                }

                if ($this->_mockery_defaultReturnValue === null) {
                    return $this->mockery_returnValueForMethod($method);
                }

                return $this->_mockery_defaultReturnValue;
            }
        }

        $message = sprintf(
            'Method %s::%s() does not exist on this mock object',
            $this->_mockery_name,
            $method
        );

        if ($rm !== null) {
            $message = sprintf(
                'Received %s::%s(), but no expectations were specified',
                $this->_mockery_name,
                $method
            );
        }

        $badMethodCallException = new BadMethodCallException($message);
        $this->_mockery_thrownExceptions[] = $badMethodCallException;
        throw $badMethodCallException;
    }

    public function _mockery_handleStaticMethodCall(string $method, array $args)
    {
        $associatedRealObject = Mockery::fetchMock($this->_mockery_name);
        try {
            return $associatedRealObject->__call($method, $args);
        } catch (BadMethodCallException $e) {
            throw new BadMethodCallException(
                'Static method ' . $associatedRealObject->mockery_getName() . '::' . $method
                . '() does not exist on this mock object',
                0,
                $e
            );
        }
    }

    public function allows($something)
    {
        if (is_string($something)) {
            return $this->shouldReceive($something);
        }

        if (empty($something)) {
            return $this->shouldReceive();
        }

        foreach ($something as $method => $returnValue) {
            $this->shouldReceive($method)
                 ->andReturn($returnValue);
        }

        return $this->mock;
    }

    public function asUndefined()
    {
        $this->_mockery_ignoreMissing = true;
        $this->_mockery_defaultReturnValue = new Undefined();

        return $this->mock;
    }

    public function byDefault()
    {
        foreach ($this->_mockery_expectations as $director) {
            $exps = $director->getExpectations();
            foreach ($exps as $exp) {
                $exp->byDefault();
            }
        }

        return $this->mock;
    }

    public function call($method, array $args = [])
    {
        return $this->_mockery_handleMethodCall($method, $args);
    }

    public function callStatic($method, array $args)
    {
        return $this->mock::_mockery_handleStaticMethodCall($method, $args);
    }

    public function destruct(): void
    {
    }

    public function expects($something): ExpectsHigherOrderMessage
    {
        if (is_string($something)) {
            return $this->shouldReceive($something)
                        ->once();
        }

        return new ExpectsHigherOrderMessage($this->mock);
    }

    public function getNonPublicMethods(): array
    {
        return array_map(
            static function ($method) {
                return $method->getName();
            },
            array_filter($this->mockery_getMethods(), static function ($method) {
                return ! $method->isPublic();
            })
        );
    }

    public function hasMethodOverloadingInParentClass(): bool
    {
        // if there's __call any name would be callable
        return is_callable(
            get_parent_class($this->_mockery_name) . '::aFunctionNameThatNoOneWouldEverUseInRealLife12345'
        );
    }

    public function isset(string $name): bool
    {
        if (
            str_starts_with($name, '_mockery_')&&
            mb_stripos($name, '_mockery_') === false
            && get_parent_class($this->_mockery_name)
            && method_exists(get_parent_class($this->_mockery_name), '__isset')
        ) {
            return call_user_func(get_parent_class($this->_mockery_name) . '::__isset', $name);
        }

        return false;
    }

    public function makePartial()
    {
        $this->_mockery_deferMissing = true;

        return $this->mock;
    }

    public function mockery_allocateOrder(): int
    {
        ++$this->_mockery_allocatedOrder;
        return $this->_mockery_allocatedOrder;
    }

    public function mockery_callSubjectMethod(string $name, array $args)
    {
        if (
            ! method_exists($this->mock, $name)
            && get_parent_class($this->mock)
            && method_exists(get_parent_class($this->mock), '__call')
        ) {
            return call_user_func(get_parent_class($this->mock) . '::__call', $name, $args);
        }
        return call_user_func_array(get_parent_class($this->mock) . '::' . $name, $args);
    }

    public function mockery_findExpectation(string $method, array $args): ?Expectation
    {
        if (! isset($this->_mockery_expectations[$method])) {
            return null;
        }
        return $this->_mockery_expectations[$method]->findExpectation($args);
    }

    public function mockery_getContainer()
    {
        if ($this->_mockery_container instanceof Container) {
            return $this->_mockery_container;
        }

        return $this->_mockery_container = new Container();
    }

    public function mockery_getCurrentOrder(): int
    {
        return $this->_mockery_currentOrder;
    }

    public function mockery_getExpectationCount(): int
    {
        $count = $this->_mockery_expectations_count;
        foreach ($this->_mockery_expectations as $director) {
            $count += $director->getExpectationCount();
        }
        return $count;
    }

    public function mockery_getExpectations(): array
    {
        return $this->_mockery_expectations;
    }

    public function mockery_getExpectationsFor(string $method): ?ExpectationDirector
    {
        if (isset($this->_mockery_expectations[$method])) {
            return $this->_mockery_expectations[$method];
        }

        return null;
    }

    public function mockery_getGroups(): array
    {
        return $this->_mockery_groups;
    }

    public function mockery_getMethod(string $name): ?ReflectionMethod
    {
        foreach ($this->mockery_getMethods() as $method) {
            if ($method->getName() === $name) {
                return $method;
            }
        }

        return null;
    }

    public function mockery_getMethods(): array
    {

        if (self::$_mockery_methods && Mockery::getConfiguration()->reflectionCacheEnabled()) {
            return self::$_mockery_methods;
        }

        if (isset($this->_mockery_partial)) {
            $reflected = new ReflectionObject($this->_mockery_partial);
        } else {
            $reflected = new ReflectionClass($this->_mockery_name);
        }

        return self::$_mockery_methods = $reflected->getMethods();
    }

    public function mockery_getMockableMethods(): array
    {
        return $this->_mockery_mockableMethods;
    }

    public function mockery_getMockableProperties(): array
    {
        return $this->_mockery_mockableProperties;
    }

    public function mockery_getName(): string
    {
        return $this->_mockery_name;
    }

    public function mockery_init(object $mock, ?Container $container, ?object $partialObject, bool $instanceMock): void
    {
       $this->_mockery_partial = $partialObject;

       $this->_mockery_container = $container ?? new Container();

       $this->_mockery_instanceMock = $instanceMock;

       $this->mock = $mock;

       if (! Mockery::getConfiguration()->mockingNonExistentMethodsAllowed()) {
           foreach ($mock->mockery_getMethods() as $method) {
               if ($method->isPublic()) {
                   $this->_mockery_mockableMethods[] = $method->getName();
               }
           }
       }
    }

    public function mockery_isAnonymous(): bool
    {
        $rfc = new ReflectionClass($this->mock);

        // PHP 8 has Stringable interface
        $interfaces = array_filter($rfc->getInterfaces(), static function ($i) {
            return $i->getName() !== 'Stringable';
        });

        return $rfc->getParentClass() === false && count($interfaces) === 2;
    }

    public function mockery_isInstance(): bool
    {
        return $this->_mockery_instanceMock;
    }

    public function mockery_returnValueForMethod(string $name)
    {
        $rm = $this->mockery_getMethod($name);

        if ($rm === null) {
            return null;
        }

        $returnType = Reflector::getSimplestReturnType($rm);

        switch ($returnType) {
            case null:     return null;
            case 'string': return '';
            case 'int':    return 0;
            case 'float':  return 0.0;
            case 'bool':   return false;
            case 'true':   return true;
            case 'false':   return false;

            case 'array':
            case 'iterable':
                return [];

            case 'callable':
            case '\Closure':
                return static function () {
                };

            case '\Traversable':
            case '\Generator':
                $generator = static function () {
                    yield;
                };
                return $generator();

            case 'void':
                return null;

            case 'static':
                return $this;

            case 'object':
                $mock = Mockery::mock();
                if ($this->_mockery_ignoreMissingRecursive) {
                    $mock->shouldIgnoreMissing($this->_mockery_defaultReturnValue, true);
                }
                return $mock;

            default:
                $mock = Mockery::mock($returnType);
                if ($this->_mockery_ignoreMissingRecursive) {
                    $mock->shouldIgnoreMissing($this->_mockery_defaultReturnValue, true);
                }
                return $mock;
        }
    }

    public function mockery_setCurrentOrder(int $order): int
    {
        $this->_mockery_currentOrder = $order;
        return $this->_mockery_currentOrder;
    }

    public function mockery_setExpectationsFor(string $method, ExpectationDirector $director): ExpectationDirector
    {
        $this->log(__METHOD__ . '|' . $method , $director);
        return $this->_mockery_expectations[$method] = $director;
    }

    public function mockery_setGroup($group, int $order)
    {
        $this->_mockery_groups[$group] = $order;

        return $this->mock;
    }

    public function mockery_teardown(): void
    {
    }

    public function mockery_thrownExceptions(): array
    {
        return $this->_mockery_thrownExceptions;
    }

    public function mockery_validateOrder(string $method, int $order): void
    {
        if ($order < $this->_mockery_currentOrder) {
            $exception = new InvalidOrderException(
                'Method ' . $this->_mockery_name . '::' . $method . '()'
                . ' called out of order: expected order '
                . $order . ', was ' . $this->_mockery_currentOrder
            );
            $exception->setMock($this->mock)
                      ->setMethodName($method)
                      ->setExpectedOrder($order)
                      ->setActualOrder($this->_mockery_currentOrder);
            throw $exception;
        }
        $this->mockery_setCurrentOrder($order);
    }

    public function mockery_verify(): void
    {
        if ($this->_mockery_verified) {
            return;
        }

        if ($this->_mockery_ignoreVerification === true) {
            return;
        }

        $this->_mockery_verified = true;
        foreach ($this->_mockery_expectations as $director) {
            $director->verify();
        }
    }

    public function name(): string
    {
        return $this->_mockery_name;
    }

    public function shouldAllowMockingMethod(string $method)
    {
        $this->_mockery_mockableMethods[] = $method;

        return $this->mock;
    }

    public function shouldAllowMockingProtectedMethods()
    {
        if (! Mockery::getConfiguration()->mockingNonExistentMethodsAllowed()) {
            foreach ($this->mockery_getMethods() as $method) {
                if ($method->isProtected()) {
                    $this->_mockery_mockableMethods[] = $method->getName();
                }
            }
        }

        $this->_mockery_allowMockingProtectedMethods = true;

        return $this->mock;
    }

    public function shouldDeferMissing()
    {
        $this->makePartial();

        return $this->mock;
    }

    public function shouldHaveBeenCalled()
    {
        return $this->shouldHaveReceived('__invoke');
    }

    public function shouldHaveReceived($method, $args = null)
    {
        if ($method === null) {
            return new HigherOrderMessage($this->mock, 'shouldHaveReceived');
        }

        $expectation = new VerificationExpectation($this->mock, $method);

        if ($args !== null) {
            $expectation->withArgs($args);
        }

        $expectation->atLeast()
                    ->once();

        $director = new VerificationDirector($this->_mockery_getReceivedMethodCalls(), $expectation);

        $this->_mockery_expectations_count++;

        $director->verify();

        return $director;
    }

    public function shouldIgnoreMissing($returnValue = null, bool $recursive = false)
    {
        $this->_mockery_ignoreMissing = true;
        $this->_mockery_ignoreMissingRecursive = $recursive;
        $this->_mockery_defaultReturnValue = $returnValue;

        return $this->mock;
    }

    public function shouldNotHaveBeenCalled(?array $args)
    {
        return $this->shouldNotHaveReceived('__invoke', $args);
    }

    public function shouldNotHaveReceived($method, $args): ?HigherOrderMessage
    {
        if ($method === null) {
            return new HigherOrderMessage($this->mock, 'shouldNotHaveReceived');
        }

        $expectation = new VerificationExpectation($this->mock, $method);
        if ($args !== null) {
            $expectation->withArgs($args);
        }
        $expectation->never();
        $director = new VerificationDirector($this->_mockery_getReceivedMethodCalls(), $expectation);
        $this->_mockery_expectations_count++;
        $director->verify();
        return null;
    }

    public function shouldNotReceive($methodNames)
    {
        if ($methodNames === []) {
            return new HigherOrderMessage($this->mock, 'shouldNotReceive');
        }

        $expectation = call_user_func_array([$this->mock, 'shouldReceive'], $methodNames);
        $expectation->never();
        return $expectation;
    }

    public function shouldReceive($methodNames)
    {
        if ($methodNames === []) {
            return new HigherOrderMessage($this->mock, 'shouldReceive');
        }

        foreach ($methodNames as $method) {
            if ($method === '') {
                throw new InvalidArgumentException('Received empty method name');
            }
        }

        $self = $this->mock;
        $allowMockingProtectedMethods = $this->_mockery_allowMockingProtectedMethods;

        return Mockery::parseShouldReturnArgs(
            $this->mock,
            $methodNames,
            static function (string $method) use ($self, $allowMockingProtectedMethods) {
                $rm = $self->mockery_getMethod($method);
                if ($rm) {
                    if ($rm->isPrivate()) {
                        throw new InvalidArgumentException("{$method}() cannot be mocked as it is a private method");
                    }
                    if (! $allowMockingProtectedMethods && $rm->isProtected()) {
                        throw new InvalidArgumentException(
                            "{$method}() cannot be mocked as it is a protected method and mocking protected methods is not enabled for the currently used mock object. Use shouldAllowMockingProtectedMethods() to enable mocking of protected methods."
                        );
                    }
                }

                $director = $self->mockery_getExpectationsFor($method);
                if (! $director) {
                    $director = new ExpectationDirector($method, $self);
                    $self->mockery_setExpectationsFor($method, $director);
                }
                $expectation = new Expectation($self, $method);
                $director->addExpectation($expectation);
                return $expectation;
            }
        );
    }

    public function toString(): string
    {
        return $this->call('__toString', []);
    }

    public function wakeup(): void
    {
    }

    public static function new(string $name): self
    {
        return new self($name);
    }

    public function construct(bool $ignoreVerification = null): void
    {
        $this->_mockery_constructorCalled(func_get_args());
    }
}

I'll need to debug and resolve the issues with the broken features to ensure that the functionality is restored without changing the current behavior.

@comes
Copy link

comes commented Feb 26, 2024

🦾 awesome! Is there any way I can help?

@Jacobs63
Copy link

Update: refactored the MockInternals as previously mentioned, (without) changing any of the current functionality.

Encountered issues with at least two features that are now broken.

  • Mocking a __construct method (only when explicitly defining expect(‘__construct'))
  • Mocking something non-existent mock() (with no args)

These issues will require some debugging to resolve.

For readonly:

  • Added a new ReadOnlyPass instance of Pass

    • Removes \AllowDynamicProperties attribute
    • Declare readonly
    • Add types to properties

Mock & MockInternals Class
I'll need to debug and resolve the issues with the broken features to ensure that the functionality is restored without changing the current behavior.

Hello, any progress on this?

I could provide assistance, if needed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Fixed for bug fixes or error corrections Major Backwards Incompatible changes
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Cannot mock readonly class
3 participants