first commit

This commit is contained in:
2026-01-25 18:18:09 +08:00
commit 509312e604
8136 changed files with 2349298 additions and 0 deletions

View File

@ -0,0 +1,32 @@
name: CI
on:
pull_request:
push:
branches-ignore:
- master
jobs:
tests:
runs-on: ubuntu-latest
strategy:
matrix:
php: [8.1, 8.2, 8.3, 8.4]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
- name: Validate composer.json
run: composer validate
- name: Install dependencies
run: composer update --prefer-source
- name: Run test suite
run: php vendor/bin/phpunit tests

View File

@ -0,0 +1,53 @@
name: Release
on:
push:
branches:
- master
jobs:
tests:
runs-on: ubuntu-latest
strategy:
matrix:
php: [8.1, 8.2, 8.3, 8.4]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
- name: Validate composer.json
run: composer validate
- name: Install dependencies
run: composer update --prefer-source
- name: Run test suite
run: php vendor/bin/phpunit tests
release:
name: Automated release
needs: [ tests ]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v1
with:
node-version: 22
- run: >
npx
-p "@semantic-release/commit-analyzer"
-p "@semantic-release/release-notes-generator"
-p conventional-changelog-conventionalcommits
-p semantic-release
-- semantic-release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
permissions:
packages: write
contents: write
pull-requests: write

3
vendor/codeception/stub/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
.idea/
/vendor/
composer.lock

11
vendor/codeception/stub/.releaserc.json vendored Normal file
View File

@ -0,0 +1,11 @@
{
"tagFormat": "${version}",
"branches": ["master"],
"plugins": [
["@semantic-release/commit-analyzer", {
"preset": "conventionalcommits"
}],
"@semantic-release/release-notes-generator",
"@semantic-release/github"
]
}

21
vendor/codeception/stub/LICENSE vendored Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 Codeception
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

89
vendor/codeception/stub/Readme.md vendored Normal file
View File

@ -0,0 +1,89 @@
# Codeception\Stub
![CI](https://github.com/Codeception/Stub/workflows/CI/badge.svg)
[![Latest Stable Version](https://poser.pugx.org/codeception/stub/v/stable)](https://packagist.org/packages/codeception/stub)
[![Total Downloads](https://poser.pugx.org/codeception/stub/downloads)](https://packagist.org/packages/codeception/stub)
[![License](https://poser.pugx.org/codeception/stub/license)](https://packagist.org/packages/codeception/stub)
Library on top of PHPUnit's mock builder providing a highly simplified syntax:
## Reference
* [Stub](https://github.com/Codeception/Stub/blob/master/docs/Stub.md) - creating stub classes using static methods
* [Stub Trait](https://github.com/Codeception/Stub/blob/master/docs/StubTrait.md) - creating stubs and mocks using trait
* [Expected](https://github.com/Codeception/Stub/blob/master/docs/Expected.md) - defining expectations for mocks
## Install
Enabled by default in Codeception.
For PHPUnit install this package:
```
composer require codeception/stub --dev
```
## Stubs
Stubs can be constructed with `Codeception\Stub` static calls:
```php
<?php
// create a stub with find method replaced
$userRepository = Stub::make(UserRepository::class, ['find' => new User]);
$userRepository->find(1); // => User
// create a dummy
$userRepository = Stub::makeEmpty(UserRepository::class);
// create a stub with all methods replaced except one
$user = Stub::makeEmptyExcept(User::class, 'validate');
$user->validate($data);
// create a stub by calling constructor and replacing a method
$user = Stub::construct(User::class, ['name' => 'davert'], ['save' => false]);
// create a stub by calling constructor with empty methods
$user = Stub::constructEmpty(User::class, ['name' => 'davert']);
// create a stub by calling constructor with empty methods
$user = Stub::constructEmptyExcept(User::class, 'getName', ['name' => 'davert']);
$user->getName(); // => davert
$user->setName('jane'); // => this method is empty
$user->getName(); // => davert
```
[See complete reference](https://github.com/Codeception/Stub/blob/master/docs/Stub.md)
Alternatively, stubs can be created by using [`Codeception\Test\Feature\Stub` trait](https://github.com/Codeception/Stub/blob/master/docs/StubTrait.md):
```php
<?php
$this->make(UserRepositry::class);
$this->makeEmpty(UserRepositry::class);
$this->construct(UserRepositry::class);
$this->constructEmpty(UserRepositry::class);
// ...
```
## Mocks
Mocks should be created by including [`Codeception\Test\Feature\Stub` trait](https://github.com/Codeception/Stub/blob/master/docs/StubTrait.md) into a test case.
Execution expectation are set with [`Codeception\Stub\Expected`](https://github.com/Codeception/Stub/blob/master/docs/Expected.md):
```php
<?php
// find should be never called
$userRepository = $this->make(UserRepository::class, [
'find' => Codeception\Stub\Expected::never()
]);
// find should be called once and return a new user
$userRepository = $this->make(UserRepository::class, [
'find' => Codeception\Stub\Expected::once(new User)
]);
```
## License
MIT

47
vendor/codeception/stub/RoboFile.php vendored Normal file
View File

@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
use Robo\Tasks;
require_once 'vendor/autoload.php';
/**
* This is project's console commands configuration for Robo task runner.
*
* @see https://robo.li/
*/
class RoboFile extends Tasks
{
protected $docs = [
'docs/Stub.md' => 'Codeception\Stub',
'docs/Expected.md' => 'Codeception\Stub\Expected',
'docs/StubTrait.md' => 'Codeception\Test\Feature\Stub',
];
public function docs()
{
foreach ($this->docs as $file => $class) {
if (!class_exists($class, true) && !trait_exists($class, true)) {
throw new Exception('ups');
}
$this->say("Here goes, $class");
$this->taskGenDoc($file)
->docClass($class)
->filterMethods(function(ReflectionMethod $method) {
if ($method->isConstructor() or $method->isDestructor()) return false;
if (!$method->isPublic()) return false;
if (strpos($method->name, '_') === 0) return false;
if (strpos($method->name, 'stub') === 0) return false;
return true;
})
->processMethodDocBlock(
function (ReflectionMethod $m, $doc) {
$doc = str_replace(array('@since'), array(' * available since version'), $doc);
$doc = str_replace(array(' @', "\n@"), array(" * ", "\n * "), $doc);
return $doc;
})
->processProperty(false)
->run();
}
}
}

22
vendor/codeception/stub/composer.json vendored Normal file
View File

@ -0,0 +1,22 @@
{
"name": "codeception/stub",
"description":"Flexible Stub wrapper for PHPUnit's Mock Builder",
"type": "library",
"license":"MIT",
"minimum-stability": "dev",
"autoload": {
"psr-4": {
"Codeception\\": "src/"
}
},
"require": {
"php": "^8.1",
"phpunit/phpunit": "^8.4 | ^9.0 | ^10.0 | ^11 | ^12"
},
"require-dev": {
"consolidation/robo": "^3.0"
},
"conflict": {
"codeception/codeception": "<5.0.6"
}
}

120
vendor/codeception/stub/docs/Expected.md vendored Normal file
View File

@ -0,0 +1,120 @@
## Codeception\Stub\Expected
#### *public static* never($params = null)
Checks if a method never has been invoked
If method invoked, it will immediately throw an
exception.
```php
<?php
use \Codeception\Stub\Expected;
$user = $this->make('User', [
'getName' => Expected::never(),
'someMethod' => function() {}
]);
$user->someMethod();
```
* `param mixed` $params
#### *public static* once($params = null)
Checks if a method has been invoked exactly one
time.
If the number is less or greater it will later be checked in verify() and also throw an
exception.
```php
<?php
use \Codeception\Stub\Expected;
$user = $this->make(
'User',
array(
'getName' => Expected::once('Davert'),
'someMethod' => function() {}
)
);
$userName = $user->getName();
$this->assertEquals('Davert', $userName);
```
Alternatively, a function can be passed as parameter:
```php
<?php
Expected::once(function() { return Faker::name(); });
```
* `param mixed` $params
#### *public static* atLeastOnce($params = null)
Checks if a method has been invoked at least one
time.
If the number of invocations is 0 it will throw an exception in verify.
```php
<?php
use \Codeception\Stub\Expected;
$user = $this->make(
'User',
array(
'getName' => Expected::atLeastOnce('Davert')),
'someMethod' => function() {}
)
);
$user->getName();
$userName = $user->getName();
$this->assertEquals('Davert', $userName);
```
Alternatively, a function can be passed as parameter:
```php
<?php
Expected::atLeastOnce(function() { return Faker::name(); });
```
* `param mixed` $params
#### *public static* exactly($count, $params = null)
Checks if a method has been invoked a certain amount
of times.
If the number of invocations exceeds the value it will immediately throw an
exception,
If the number is less it will later be checked in verify() and also throw an
exception.
``` php
<?php
use \Codeception\Stub;
use \Codeception\Stub\Expected;
$user = $this->make(
'User',
array(
'getName' => Expected::exactly(3, 'Davert'),
'someMethod' => function() {}
)
);
$user->getName();
$user->getName();
$userName = $user->getName();
$this->assertEquals('Davert', $userName);
```
Alternatively, a function can be passed as parameter:
```php
<?php
Expected::exactly(function() { return Faker::name() });
```
* `param mixed` $params

308
vendor/codeception/stub/docs/Stub.md vendored Normal file
View File

@ -0,0 +1,308 @@
## Codeception\Stub
#### *public static* make($class, array $params = Array ( ) , $testCase = null)
Instantiates a class without executing a constructor.
Properties and methods can be set as a second parameter.
Even protected and private properties can be set.
```php
<?php
Stub::make('User');
Stub::make('User', ['name' => 'davert']);
```
Accepts either name of class or object of that class
```php
<?php
Stub::make(new User, ['name' => 'davert']);
```
To replace method provide it's name as a key in second parameter
and it's return value or callback function as parameter
```php
<?php
Stub::make('User', ['save' => function () { return true; }]);
Stub::make('User', ['save' => true]);
```
**To create a mock, pass current testcase name as last argument:**
```php
<?php
Stub::make('User', [
'save' => \Codeception\Stub\Expected::once()
], $this);
```
* template RealInstanceType of object
* `param class-string<RealInstanceType>|RealInstanceType|callable(): class-string<RealInstanceType>` $class - A class to be mocked
* `param array` $params - properties and methods to set
* `param bool|PHPUnitTestCase` $testCase
* return PHPUnitMockObject&RealInstanceType - mock
* throws RuntimeException when class does not exist
* throws Exception
#### *public static* factory($class, $num = 1, array $params = Array ( ) )
Creates $num instances of class through `Stub::make`.
* `param mixed` $class
* throws Exception
#### *public static* makeEmptyExcept($class, $method, array $params = Array ( ) , $testCase = null)
Instantiates class having all methods replaced with dummies except one.
Constructor is not triggered.
Properties and methods can be replaced.
Even protected and private properties can be set.
```php
<?php
Stub::makeEmptyExcept('User', 'save');
Stub::makeEmptyExcept('User', 'save', ['name' => 'davert']);
```
Accepts either name of class or object of that class
```php
<?php
* Stub::makeEmptyExcept(new User, 'save');
```
To replace method provide it's name as a key in second parameter
and it's return value or callback function as parameter
```php
<?php
Stub::makeEmptyExcept('User', 'save', ['isValid' => function () { return true; }]);
Stub::makeEmptyExcept('User', 'save', ['isValid' => true]);
```
**To create a mock, pass current testcase name as last argument:**
```php
<?php
Stub::makeEmptyExcept('User', 'validate', [
'save' => \Codeception\Stub\Expected::once()
], $this);
```
* template
* `param class-string<RealInstanceType>|RealInstanceType|callable(): class-string<RealInstanceType>` $class - A class to be mocked
* `param string` $method
* `param array` $params
* `param bool|PHPUnitTestCase` $testCase
* return PHPUnitMockObject&RealInstanceType
* throws Exception
#### *public static* makeEmpty($class, array $params = Array ( ) , $testCase = null)
Instantiates class having all methods replaced with dummies.
Constructor is not triggered.
Properties and methods can be set as a second parameter.
Even protected and private properties can be set.
```php
<?php
Stub::makeEmpty('User');
Stub::makeEmpty('User', ['name' => 'davert']);
```
Accepts either name of class or object of that class
```php
<?php
Stub::makeEmpty(new User, ['name' => 'davert']);
```
To replace method provide it's name as a key in second parameter
and it's return value or callback function as parameter
```php
<?php
Stub::makeEmpty('User', ['save' => function () { return true; }]);
Stub::makeEmpty('User', ['save' => true]);
```
**To create a mock, pass current testcase name as last argument:**
```php
<?php
Stub::makeEmpty('User', [
'save' => \Codeception\Stub\Expected::once()
], $this);
```
* template RealInstanceType of object
* `param class-string<RealInstanceType>|RealInstanceType|callable(): class-string<RealInstanceType>` $class - A class to be mocked
* `param bool|PHPUnitTestCase` $testCase
* return PHPUnitMockObject&RealInstanceType
* throws Exception
#### *public static* copy($obj, array $params = Array ( ) )
Clones an object and redefines it's properties (even protected and private)
* `param` $obj
* `param array` $params
* return mixed
* throws Exception
#### *public static* construct($class, array $constructorParams = Array ( ) , array $params = Array ( ) , $testCase = null)
Instantiates a class instance by running constructor.
Parameters for constructor passed as second argument
Properties and methods can be set in third argument.
Even protected and private properties can be set.
```php
<?php
Stub::construct('User', ['autosave' => false]);
Stub::construct('User', ['autosave' => false], ['name' => 'davert']);
```
Accepts either name of class or object of that class
```php
<?php
Stub::construct(new User, ['autosave' => false], ['name' => 'davert']);
```
To replace method provide it's name as a key in third parameter
and it's return value or callback function as parameter
```php
<?php
Stub::construct('User', [], ['save' => function () { return true; }]);
Stub::construct('User', [], ['save' => true]);
```
**To create a mock, pass current testcase name as last argument:**
```php
<?php
Stub::construct('User', [], [
'save' => \Codeception\Stub\Expected::once()
], $this);
```
* template RealInstanceType of object
* `param class-string<RealInstanceType>|RealInstanceType|callable(): class-string<RealInstanceType>` $class - A class to be mocked
* `param bool|PHPUnitTestCase` $testCase
* return PHPUnitMockObject&RealInstanceType
* throws Exception
#### *public static* constructEmpty($class, array $constructorParams = Array ( ) , array $params = Array ( ) , $testCase = null)
Instantiates a class instance by running constructor with all methods replaced with dummies.
Parameters for constructor passed as second argument
Properties and methods can be set in third argument.
Even protected and private properties can be set.
```php
<?php
Stub::constructEmpty('User', ['autosave' => false]);
Stub::constructEmpty('User', ['autosave' => false], ['name' => 'davert']);
```
Accepts either name of class or object of that class
```php
<?php
Stub::constructEmpty(new User, ['autosave' => false], ['name' => 'davert']);
```
To replace method provide it's name as a key in third parameter
and it's return value or callback function as parameter
```php
<?php
Stub::constructEmpty('User', [], ['save' => function () { return true; }]);
Stub::constructEmpty('User', [], ['save' => true]);
```
**To create a mock, pass current testcase name as last argument:**
```php
<?php
Stub::constructEmpty('User', [], [
'save' => \Codeception\Stub\Expected::once()
], $this);
```
* template RealInstanceType of object
* `param class-string<RealInstanceType>|RealInstanceType|callable(): class-string<RealInstanceType>` $class - A class to be mocked
* `param array` $constructorParams
* `param array` $params
* `param bool|PHPUnitTestCase` $testCase
* return PHPUnitMockObject&RealInstanceType
* throws ReflectionException
#### *public static* constructEmptyExcept($class, $method, array $constructorParams = Array ( ) , array $params = Array ( ) , $testCase = null)
Instantiates a class instance by running constructor with all methods replaced with dummies, except one.
Parameters for constructor passed as second argument
Properties and methods can be set in third argument.
Even protected and private properties can be set.
```php
<?php
Stub::constructEmptyExcept('User', 'save');
Stub::constructEmptyExcept('User', 'save', ['autosave' => false], ['name' => 'davert']);
```
Accepts either name of class or object of that class
```php
<?php
Stub::constructEmptyExcept(new User, 'save', ['autosave' => false], ['name' => 'davert']);
```
To replace method provide it's name as a key in third parameter
and it's return value or callback function as parameter
```php
<?php
Stub::constructEmptyExcept('User', 'save', [], ['save' => function () { return true; }]);
Stub::constructEmptyExcept('User', 'save', [], ['save' => true]);
```
**To create a mock, pass current testcase name as last argument:**
```php
<?php
Stub::constructEmptyExcept('User', 'save', [], [
'save' => \Codeception\Stub\Expected::once()
], $this);
```
* template RealInstanceType of object
* `param class-string<RealInstanceType>|RealInstanceType|callable(): class-string<RealInstanceType>` $class - A class to be mocked
* `param bool|PHPUnitTestCase` $testCase
* return PHPUnitMockObject&RealInstanceType
* throws ReflectionException
#### *public static* update($mock, array $params)
Replaces properties of current stub
* `param PHPUnitMockObject|object` $mock
* `param array` $params
* return object
throws LogicException
#### *public static* consecutive()
Stubbing a method call to return a list of values in the specified order.
```php
<?php
$user = Stub::make('User', ['getName' => Stub::consecutive('david', 'emma', 'sam', 'amy')]);
$user->getName(); //david
$user->getName(); //emma
$user->getName(); //sam
$user->getName(); //amy
```

View File

@ -0,0 +1,230 @@
## Codeception\Test\Feature\Stub
### Usage in Codeception
Since Codeception 2.3.8 this trait is enabled in `\Codeception\Test\Unit` class.
### Usage in PHPUnit
Include this trait into a TestCase to be able to use Stubs and Mocks:
```php
<?php
class MyTest extends \PHPUnit\Framework\TestCase
{
use Codeception\Test\Feature\Stub;
}
```
#### *public* make($class, array $params = Array ( ) )
Instantiates a class without executing a constructor.
Properties and methods can be set as a second parameter.
Even protected and private properties can be set.
``` php
<?php
$this->make('User');
$this->make('User', ['name' => 'davert']);
```
Accepts either name of class or object of that class
``` php
<?php
$this->make(new User, ['name' => 'davert']);
```
To replace method provide it's name as a key in second parameter
and it's return value or callback function as parameter
``` php
<?php
$this->make('User', ['save' => function () { return true; }]);
$this->make('User', ['save' => true]);
```
* template RealInstanceType of object
* `param class-string<RealInstanceType>|RealInstanceType|callable(): class-string<RealInstanceType>` $class - A class to be mocked
* `param array` $params - properties and methods to set
* return MockObject&RealInstanceType - mock
* throws RuntimeException when class does not exist
* throws Exception
#### *public* makeEmpty($class, array $params = Array ( ) )
Instantiates class having all methods replaced with dummies.
Constructor is not triggered.
Properties and methods can be set as a second parameter.
Even protected and private properties can be set.
``` php
<?php
$this->makeEmpty('User');
$this->makeEmpty('User', ['name' => 'davert']);
```
Accepts either name of class or object of that class
``` php
<?php
$this->makeEmpty(new User, ['name' => 'davert']);
```
To replace method provide it's name as a key in second parameter
and it's return value or callback function as parameter
``` php
<?php
$this->makeEmpty('User', ['save' => function () { return true; }]);
$this->makeEmpty('User', ['save' => true]);
```
* template RealInstanceType of object
* `param class-string<RealInstanceType>|RealInstanceType|callable(): class-string<RealInstanceType>` $class - A class to be mocked
* `param array` $params
* return MockObject&RealInstanceType
* throws Exception
#### *public* makeEmptyExcept($class, $method, array $params = Array ( ) )
Instantiates class having all methods replaced with dummies except one.
Constructor is not triggered.
Properties and methods can be replaced.
Even protected and private properties can be set.
``` php
<?php
$this->makeEmptyExcept('User', 'save');
$this->makeEmptyExcept('User', 'save', ['name' => 'davert']);
```
Accepts either name of class or object of that class
``` php
<?php
* $this->makeEmptyExcept(new User, 'save');
```
To replace method provide it's name as a key in second parameter
and it's return value or callback function as parameter
``` php
<?php
$this->makeEmptyExcept('User', 'save', ['isValid' => function () { return true; }]);
$this->makeEmptyExcept('User', 'save', ['isValid' => true]);
```
* template RealInstanceType of object
* `param class-string<RealInstanceType>|RealInstanceType|callable(): class-string<RealInstanceType>` $class - A class to be mocked
* return \PHPUnit\Framework\MockObject\MockObject&RealInstanceType
* throws Exception
#### *public* construct($class, array $constructorParams = Array ( ) , array $params = Array ( ) )
Instantiates a class instance by running constructor.
Parameters for constructor passed as second argument
Properties and methods can be set in third argument.
Even protected and private properties can be set.
``` php
<?php
$this->construct('User', ['autosave' => false]);
$this->construct('User', ['autosave' => false], ['name' => 'davert']);
```
Accepts either name of class or object of that class
``` php
<?php
$this->construct(new User, ['autosave' => false), ['name' => 'davert']);
```
To replace method provide it's name as a key in third parameter
and it's return value or callback function as parameter
``` php
<?php
$this->construct('User', [], ['save' => function () { return true; }]);
$this->construct('User', [], ['save' => true]);
```
* template RealInstanceType of object
* `param class-string<RealInstanceType>|RealInstanceType|callable(): class-string<RealInstanceType>` $class - A class to be mocked
* return MockObject&RealInstanceType
* throws Exception
#### *public* constructEmpty($class, array $constructorParams = Array ( ) , array $params = Array ( ) )
Instantiates a class instance by running constructor with all methods replaced with dummies.
Parameters for constructor passed as second argument
Properties and methods can be set in third argument.
Even protected and private properties can be set.
``` php
<?php
$this->constructEmpty('User', ['autosave' => false]);
$this->constructEmpty('User', ['autosave' => false), ['name' => 'davert']);
```
Accepts either name of class or object of that class
``` php
<?php
$this->constructEmpty(new User, ['autosave' => false], ['name' => 'davert']);
```
To replace method provide it's name as a key in third parameter
and it's return value or callback function as parameter
``` php
<?php
$this->constructEmpty('User', array(), array('save' => function () { return true; }));
$this->constructEmpty('User', array(), array('save' => true));
```
**To create a mock, pass current testcase name as last argument:**
```php
<?php
$this->constructEmpty('User', [], [
'save' => \Codeception\Stub\Expected::once()
]);
```
* template RealInstanceType of object
* `param class-string<RealInstanceType>|RealInstanceType|callable(): class-string<RealInstanceType>` $class - A class to be mocked
* return MockObject&RealInstanceType
#### *public* constructEmptyExcept($class, $method, array $constructorParams = Array ( ) , array $params = Array ( ) )
Instantiates a class instance by running constructor with all methods replaced with dummies, except one.
Parameters for constructor passed as second argument
Properties and methods can be set in third argument.
Even protected and private properties can be set.
``` php
<?php
$this->constructEmptyExcept('User', 'save');
$this->constructEmptyExcept('User', 'save', ['autosave' => false], ['name' => 'davert']);
```
Accepts either name of class or object of that class
``` php
<?php
$this->constructEmptyExcept(new User, 'save', ['autosave' => false], ['name' => 'davert']);
```
To replace method provide it's name as a key in third parameter
and it's return value or callback function as parameter
``` php
<?php
$this->constructEmptyExcept('User', 'save', [], ['save' => function () { return true; }]);
$this->constructEmptyExcept('User', 'save', [], ['save' => true]);
```
* template RealInstanceType of object
* `param class-string<RealInstanceType>|RealInstanceType|callable(): class-string<RealInstanceType>` $class - A class to be mocked
* return MockObject&RealInstanceType

656
vendor/codeception/stub/src/Stub.php vendored Normal file
View File

@ -0,0 +1,656 @@
<?php
declare(strict_types=1);
namespace Codeception;
use Closure;
use Codeception\Stub\ConsecutiveMap;
use Codeception\Stub\StubMarshaler;
use Exception;
use LogicException;
use PHPUnit\Framework\MockObject\Generator as LegacyGenerator;
use PHPUnit\Framework\MockObject\Generator\Generator;
use PHPUnit\Framework\MockObject\MockObject as PHPUnitMockObject;
use PHPUnit\Framework\MockObject\Rule\AnyInvokedCount;
use PHPUnit\Framework\MockObject\Stub\ConsecutiveCalls;
use PHPUnit\Framework\MockObject\Stub\ReturnCallback;
use PHPUnit\Framework\MockObject\Stub\ReturnStub;
use PHPUnit\Framework\TestCase as PHPUnitTestCase;
use PHPUnit\Runner\Version as PHPUnitVersion;
use ReflectionClass;
use ReflectionException;
use RuntimeException;
class Stub
{
public static array $magicMethods = ['__isset', '__get', '__set'];
/**
* Instantiates a class without executing a constructor.
* Properties and methods can be set as a second parameter.
* Even protected and private properties can be set.
*
* ```php
* <?php
* Stub::make('User');
* Stub::make('User', ['name' => 'davert']);
* ```
*
* Accepts either name of class or object of that class
*
* ```php
* <?php
* Stub::make(new User, ['name' => 'davert']);
* ```
*
* To replace method provide it's name as a key in second parameter
* and it's return value or callback function as parameter
*
* ```php
* <?php
* Stub::make('User', ['save' => function () { return true; }]);
* Stub::make('User', ['save' => true]);
* ```
*
* **To create a mock, pass current testcase name as last argument:**
*
* ```php
* <?php
* Stub::make('User', [
* 'save' => \Codeception\Stub\Expected::once()
* ], $this);
* ```
*
* @template RealInstanceType of object
* @param class-string<RealInstanceType>|RealInstanceType|callable(): class-string<RealInstanceType> $class - A class to be mocked
* @param array $params - properties and methods to set
* @param bool|PHPUnitTestCase $testCase
*
* @return PHPUnitMockObject&RealInstanceType - mock
* @throws RuntimeException when class does not exist
* @throws Exception
*/
public static function make($class, array $params = [], $testCase = false)
{
$class = self::getClassname($class);
if (!class_exists($class)) {
if (interface_exists($class)) {
throw new RuntimeException("Stub::make can't mock interfaces, please use Stub::makeEmpty instead.");
}
throw new RuntimeException("Stubbed class $class doesn't exist.");
}
$reflection = new ReflectionClass($class);
$callables = self::getMethodsToReplace($reflection, $params);
if ($reflection->isAbstract()) {
$arguments = empty($callables) ? [] : array_keys($callables);
$mock = self::generateMockForAbstractClass($class, $arguments, '', false, $testCase);
} else {
$arguments = empty($callables) ? null : array_keys($callables);
$mock = self::generateMock($class, $arguments, [], '', false, $testCase);
}
self::bindParameters($mock, $params);
return $mock;
}
/**
* Creates $num instances of class through `Stub::make`.
*
* @param mixed $class
* @throws Exception
*/
public static function factory($class, int $num = 1, array $params = []): array
{
$objects = [];
for ($i = 0; $i < $num; $i++) {
$objects[] = self::make($class, $params);
}
return $objects;
}
/**
* Instantiates class having all methods replaced with dummies except one.
* Constructor is not triggered.
* Properties and methods can be replaced.
* Even protected and private properties can be set.
*
* ```php
* <?php
* Stub::makeEmptyExcept('User', 'save');
* Stub::makeEmptyExcept('User', 'save', ['name' => 'davert']);
* ```
*
* Accepts either name of class or object of that class
*
* ```php
* <?php
* * Stub::makeEmptyExcept(new User, 'save');
* ```
*
* To replace method provide it's name as a key in second parameter
* and it's return value or callback function as parameter
*
* ```php
* <?php
* Stub::makeEmptyExcept('User', 'save', ['isValid' => function () { return true; }]);
* Stub::makeEmptyExcept('User', 'save', ['isValid' => true]);
* ```
*
* **To create a mock, pass current testcase name as last argument:**
*
* ```php
* <?php
* Stub::makeEmptyExcept('User', 'validate', [
* 'save' => \Codeception\Stub\Expected::once()
* ], $this);
* ```
* @template RealInstanceType of object
* @param class-string<RealInstanceType>|RealInstanceType|callable(): class-string<RealInstanceType> $class - A class to be mocked
* @param string $method
* @param array $params
* @param bool|PHPUnitTestCase $testCase
*
* @return PHPUnitMockObject&RealInstanceType
* @throws Exception
*/
public static function makeEmptyExcept($class, string $method, array $params = [], $testCase = false)
{
[$class, $methods] = self::createEmpty($class, $method);
$mock = self::generateMock($class, $methods, [], '', false, $testCase);
self::bindParameters($mock, $params);
return $mock;
}
/**
* Instantiates class having all methods replaced with dummies.
* Constructor is not triggered.
* Properties and methods can be set as a second parameter.
* Even protected and private properties can be set.
*
* ```php
* <?php
* Stub::makeEmpty('User');
* Stub::makeEmpty('User', ['name' => 'davert']);
* ```
*
* Accepts either name of class or object of that class
*
* ```php
* <?php
* Stub::makeEmpty(new User, ['name' => 'davert']);
* ```
*
* To replace method provide it's name as a key in second parameter
* and it's return value or callback function as parameter
*
* ```php
* <?php
* Stub::makeEmpty('User', ['save' => function () { return true; }]);
* Stub::makeEmpty('User', ['save' => true]);
* ```
*
* **To create a mock, pass current testcase name as last argument:**
*
* ```php
* <?php
* Stub::makeEmpty('User', [
* 'save' => \Codeception\Stub\Expected::once()
* ], $this);
* ```
*
* @template RealInstanceType of object
* @param class-string<RealInstanceType>|RealInstanceType|callable(): class-string<RealInstanceType> $class - A class to be mocked
* @param bool|PHPUnitTestCase $testCase
*
* @return PHPUnitMockObject&RealInstanceType
* @throws Exception
*/
public static function makeEmpty($class, array $params = [], $testCase = false)
{
$class = self::getClassname($class);
$methods = array_filter(
get_class_methods($class),
fn($i) => !in_array($i, Stub::$magicMethods)
);
$mock = self::generateMock($class, $methods, [], '', false, $testCase);
self::bindParameters($mock, $params);
return $mock;
}
/**
* Clones an object and redefines it's properties (even protected and private)
*
* @param $obj
* @param array $params
* @return mixed
* @throws Exception
*/
public static function copy($obj, array $params = [])
{
$copy = clone($obj);
self::bindParameters($copy, $params);
return $copy;
}
/**
* Instantiates a class instance by running constructor.
* Parameters for constructor passed as second argument
* Properties and methods can be set in third argument.
* Even protected and private properties can be set.
*
* ```php
* <?php
* Stub::construct('User', ['autosave' => false]);
* Stub::construct('User', ['autosave' => false], ['name' => 'davert']);
* ```
*
* Accepts either name of class or object of that class
*
* ```php
* <?php
* Stub::construct(new User, ['autosave' => false], ['name' => 'davert']);
* ```
*
* To replace method provide it's name as a key in third parameter
* and it's return value or callback function as parameter
*
* ```php
* <?php
* Stub::construct('User', [], ['save' => function () { return true; }]);
* Stub::construct('User', [], ['save' => true]);
* ```
*
* **To create a mock, pass current testcase name as last argument:**
*
* ```php
* <?php
* Stub::construct('User', [], [
* 'save' => \Codeception\Stub\Expected::once()
* ], $this);
* ```
*
* @template RealInstanceType of object
* @param class-string<RealInstanceType>|RealInstanceType|callable(): class-string<RealInstanceType> $class - A class to be mocked
* @param bool|PHPUnitTestCase $testCase
*
* @return PHPUnitMockObject&RealInstanceType
* @throws Exception
*/
public static function construct($class, array $constructorParams = [], array $params = [], $testCase = false)
{
$class = self::getClassname($class);
$reflection = new ReflectionClass($class);
$callables = self::getMethodsToReplace($reflection, $params);
$arguments = empty($callables) ? null : array_keys($callables);
$mock = self::generateMock($class, $arguments, $constructorParams, $testCase);
self::bindParameters($mock, $params);
return $mock;
}
/**
* Instantiates a class instance by running constructor with all methods replaced with dummies.
* Parameters for constructor passed as second argument
* Properties and methods can be set in third argument.
* Even protected and private properties can be set.
*
* ```php
* <?php
* Stub::constructEmpty('User', ['autosave' => false]);
* Stub::constructEmpty('User', ['autosave' => false], ['name' => 'davert']);
* ```
*
* Accepts either name of class or object of that class
*
* ```php
* <?php
* Stub::constructEmpty(new User, ['autosave' => false], ['name' => 'davert']);
* ```
*
* To replace method provide it's name as a key in third parameter
* and it's return value or callback function as parameter
*
* ```php
* <?php
* Stub::constructEmpty('User', [], ['save' => function () { return true; }]);
* Stub::constructEmpty('User', [], ['save' => true]);
* ```
*
* **To create a mock, pass current testcase name as last argument:**
*
* ```php
* <?php
* Stub::constructEmpty('User', [], [
* 'save' => \Codeception\Stub\Expected::once()
* ], $this);
* ```
*
* @template RealInstanceType of object
* @param class-string<RealInstanceType>|RealInstanceType|callable(): class-string<RealInstanceType> $class - A class to be mocked
* @param array $constructorParams
* @param array $params
* @param bool|PHPUnitTestCase $testCase
*
* @return PHPUnitMockObject&RealInstanceType
*/
public static function constructEmpty($class, array $constructorParams = [], array $params = [], $testCase = false)
{
$class = self::getClassname($class);
$methods = array_filter(
get_class_methods($class),
fn($i) => !in_array($i, Stub::$magicMethods)
);
$mock = self::generateMock($class, $methods, $constructorParams, $testCase);
self::bindParameters($mock, $params);
return $mock;
}
/**
* Instantiates a class instance by running constructor with all methods replaced with dummies, except one.
* Parameters for constructor passed as second argument
* Properties and methods can be set in third argument.
* Even protected and private properties can be set.
*
* ```php
* <?php
* Stub::constructEmptyExcept('User', 'save');
* Stub::constructEmptyExcept('User', 'save', ['autosave' => false], ['name' => 'davert']);
* ```
*
* Accepts either name of class or object of that class
*
* ```php
* <?php
* Stub::constructEmptyExcept(new User, 'save', ['autosave' => false], ['name' => 'davert']);
* ```
*
* To replace method provide it's name as a key in third parameter
* and it's return value or callback function as parameter
*
* ```php
* <?php
* Stub::constructEmptyExcept('User', 'save', [], ['save' => function () { return true; }]);
* Stub::constructEmptyExcept('User', 'save', [], ['save' => true]);
* ```
*
* **To create a mock, pass current testcase name as last argument:**
*
* ```php
* <?php
* Stub::constructEmptyExcept('User', 'save', [], [
* 'save' => \Codeception\Stub\Expected::once()
* ], $this);
* ```
*
* @template RealInstanceType of object
* @param class-string<RealInstanceType>|RealInstanceType|callable(): class-string<RealInstanceType> $class - A class to be mocked
* @param bool|PHPUnitTestCase $testCase
*
* @return PHPUnitMockObject&RealInstanceType
* @throws ReflectionException
*/
public static function constructEmptyExcept(
$class,
string $method,
array $constructorParams = [],
array $params = [],
$testCase = false
) {
[$class, $methods] = self::createEmpty($class, $method);
$mock = self::generateMock($class, $methods, $constructorParams, $testCase);
self::bindParameters($mock, $params);
return $mock;
}
private static function generateMock()
{
$args = func_get_args();
// PHPUnit 11 added the optional parameter $markAsMockObject:
// https://github.com/sebastianbergmann/phpunit/commit/db9ae302fe1ad89451ecfacc850e88ab7c6df5a3
// The parameter was removed in PHPUnit 12:
// https://github.com/sebastianbergmann/phpunit/commit/a98e3939c74f6103cbeb7a785b73eb4a10784474
if (version_compare(PHPUnitVersion::series(), '11', '>=') && version_compare(PHPUnitVersion::series(), '12', '<')) {
if (!is_bool($args[1]) || !is_bool($args[2])) {
$additionalParameters = [];
if (!is_bool($args[1])) {
$additionalParameters[] = true;
}
if (!is_bool($args[2])) {
$additionalParameters[] = true;
}
array_splice($args, 1, 0, $additionalParameters);
}
} elseif (version_compare(PHPUnitVersion::series(), '10.4', '>=') && !is_bool($args[1])) {
array_splice($args, 1, 0, [true]);
}
return self::doGenerateMock($args);
}
/**
* Returns a mock object for the specified abstract class with all abstract
* methods of the class mocked. Concrete methods to mock can be specified with
* the last parameter
*
* @since Method available since Release 1.0.0
*/
private static function generateMockForAbstractClass(): object
{
return self::doGenerateMock(func_get_args(), true);
}
private static function doGenerateMock($args, $isAbstract = false)
{
$testCase = self::extractTestCaseFromArgs($args);
// PHPUnit 10.4 changed method names
if (version_compare(PHPUnitVersion::series(), '10.4', '>=')) {
$methodName = $isAbstract ? 'mockObjectForAbstractClass' : 'testDouble';
} else {
$methodName = $isAbstract ? 'getMockForAbstractClass' : 'getMock';
}
if ($isAbstract && version_compare(PHPUnitVersion::series(), '12', '>=')) {
throw new RuntimeException('PHPUnit 12 or greater does not allow to mock abstract classes anymore');
}
// PHPUnit 10.3 changed the namespace
if (version_compare(PHPUnitVersion::series(), '10.3', '>=')) {
$generatorClass = new Generator();
} else {
$generatorClass = new LegacyGenerator();
}
$mock = call_user_func_array([$generatorClass, $methodName], $args);
if ($testCase instanceof PHPUnitTestCase) {
if (version_compare(PHPUnitVersion::series(), '12.5', '>=')) {
$testCase->registerMockObject($mock::class, $mock);
} else {
$testCase->registerMockObject($mock);
}
}
return $mock;
}
private static function extractTestCaseFromArgs(&$args)
{
$argsLength = count($args) - 1;
$testCase = $args[$argsLength];
unset($args[$argsLength]);
return $testCase;
}
/**
* Replaces properties of current stub
*
* @param PHPUnitMockObject|object $mock
* @param array $params
* @return object
*@throws LogicException
*/
public static function update(object $mock, array $params): object
{
self::bindParameters($mock, $params);
return $mock;
}
/**
* @param PHPUnitMockObject|object $mock
* @param array $params
* @throws LogicException
*/
protected static function bindParameters($mock, array $params)
{
$reflectionClass = new ReflectionClass($mock);
if ($mock instanceof PHPUnitMockObject) {
$parentClass = $reflectionClass->getParentClass();
if ($parentClass !== false) {
$reflectionClass = $reflectionClass->getParentClass();
}
}
foreach ($params as $param => $value) {
// redefine method
if ($reflectionClass->hasMethod($param)) {
if ($value instanceof StubMarshaler) {
$marshaler = $value;
$mock
->expects($marshaler->getMatcher())
->method($param)
->will(new ReturnCallback($marshaler->getValue()));
} elseif ($value instanceof Closure) {
$mock
->expects(new AnyInvokedCount)
->method($param)
->will(new ReturnCallback($value));
} elseif ($value instanceof ConsecutiveMap) {
$consecutiveMap = $value;
$mock
->expects(new AnyInvokedCount)
->method($param)
->will(new ConsecutiveCalls($consecutiveMap->getMap()));
} else {
$mock
->expects(new AnyInvokedCount)
->method($param)
->will(new ReturnStub($value));
}
} elseif ($reflectionClass->hasProperty($param)) {
$reflectionProperty = $reflectionClass->getProperty($param);
$reflectionProperty->setValue($mock, $value);
} else {
if ($reflectionClass->hasMethod('__set')) {
try {
$mock->{$param} = $value;
} catch (Exception $exception) {
throw new LogicException(
sprintf(
'Could not add property %1$s, class %2$s implements __set method, '
. 'and no %1$s property exists',
$param,
$reflectionClass->getName()
),
$exception->getCode(),
$exception
);
}
} else {
$mock->{$param} = $value;
}
}
}
}
/**
* @TO-DO Should be simplified
*/
protected static function getClassname($object)
{
if (is_object($object)) {
return get_class($object);
}
if (is_callable($object)) {
return call_user_func($object);
}
return $object;
}
protected static function getMethodsToReplace(ReflectionClass $reflection, array $params): array
{
$callables = [];
foreach ($params as $method => $value) {
if ($reflection->hasMethod($method)) {
$callables[$method] = $value;
}
}
return $callables;
}
/**
* Stubbing a method call to return a list of values in the specified order.
*
* ```php
* <?php
* $user = Stub::make('User', ['getName' => Stub::consecutive('david', 'emma', 'sam', 'amy')]);
* $user->getName(); //david
* $user->getName(); //emma
* $user->getName(); //sam
* $user->getName(); //amy
* ```
*/
public static function consecutive(): ConsecutiveMap
{
return new ConsecutiveMap(func_get_args());
}
/**
* @param mixed $class
* @throws ReflectionException
*/
private static function createEmpty($class, string $method): array
{
$class = self::getClassname($class);
$reflectionClass = new ReflectionClass($class);
$methods = $reflectionClass->getMethods();
$methods = array_filter(
$methods,
fn($m) => !in_array($m->name, Stub::$magicMethods)
);
$methods = array_filter(
$methods,
fn($m) => $method != $m->name
);
$methods = array_map(
fn($m) => $m->name,
$methods
);
$methods = count($methods) ? $methods : null;
return [$class, $methods];
}
}

View File

@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace Codeception\Stub;
/**
* Holds matcher and value of mocked method
*/
class ConsecutiveMap
{
private array $consecutiveMap = [];
public function __construct(array $consecutiveMap)
{
$this->consecutiveMap = $consecutiveMap;
}
public function getMap(): array
{
return $this->consecutiveMap;
}
}

View File

@ -0,0 +1,167 @@
<?php
declare(strict_types=1);
namespace Codeception\Stub;
use Closure;
use PHPUnit\Framework\MockObject\Rule\InvokedAtLeastOnce;
use PHPUnit\Framework\MockObject\Rule\InvokedCount;
class Expected
{
/**
* Checks if a method never has been invoked
*
* If method invoked, it will immediately throw an
* exception.
*
* ```php
* <?php
* use \Codeception\Stub\Expected;
*
* $user = $this->make('User', [
* 'getName' => Expected::never(),
* 'someMethod' => function() {}
* ]);
* $user->someMethod();
* ```
*
* @param mixed $params
*/
public static function never($params = null): StubMarshaler
{
return new StubMarshaler(
new InvokedCount(0),
self::closureIfNull($params)
);
}
/**
* Checks if a method has been invoked exactly one
* time.
*
* If the number is less or greater it will later be checked in verify() and also throw an
* exception.
*
* ```php
* <?php
* use \Codeception\Stub\Expected;
*
* $user = $this->make(
* 'User',
* array(
* 'getName' => Expected::once('Davert'),
* 'someMethod' => function() {}
* )
* );
* $userName = $user->getName();
* $this->assertEquals('Davert', $userName);
* ```
* Alternatively, a function can be passed as parameter:
*
* ```php
* <?php
* Expected::once(function() { return Faker::name(); });
* ```
*
* @param mixed $params
*/
public static function once($params = null): StubMarshaler
{
return new StubMarshaler(
new InvokedCount(1),
self::closureIfNull($params)
);
}
/**
* Checks if a method has been invoked at least one
* time.
*
* If the number of invocations is 0 it will throw an exception in verify.
*
* ```php
* <?php
* use \Codeception\Stub\Expected;
*
* $user = $this->make(
* 'User',
* array(
* 'getName' => Expected::atLeastOnce('Davert')),
* 'someMethod' => function() {}
* )
* );
* $user->getName();
* $userName = $user->getName();
* $this->assertEquals('Davert', $userName);
* ```
*
* Alternatively, a function can be passed as parameter:
*
* ```php
* <?php
* Expected::atLeastOnce(function() { return Faker::name(); });
* ```
*
* @param mixed $params
*/
public static function atLeastOnce($params = null): StubMarshaler
{
return new StubMarshaler(
new InvokedAtLeastOnce(),
self::closureIfNull($params)
);
}
/**
* Checks if a method has been invoked a certain amount
* of times.
* If the number of invocations exceeds the value it will immediately throw an
* exception,
* If the number is less it will later be checked in verify() and also throw an
* exception.
*
* ``` php
* <?php
* use \Codeception\Stub;
* use \Codeception\Stub\Expected;
*
* $user = $this->make(
* 'User',
* array(
* 'getName' => Expected::exactly(3, 'Davert'),
* 'someMethod' => function() {}
* )
* );
* $user->getName();
* $user->getName();
* $userName = $user->getName();
* $this->assertEquals('Davert', $userName);
* ```
* Alternatively, a function can be passed as parameter:
*
* ```php
* <?php
* Expected::exactly(function() { return Faker::name() });
* ```
*
* @param mixed $params
*/
public static function exactly(int $count, $params = null): StubMarshaler
{
return new StubMarshaler(
new InvokedCount($count),
self::closureIfNull($params)
);
}
private static function closureIfNull($params): Closure
{
if ($params instanceof Closure) {
return $params;
}
return fn() => $params;
}
}

View File

@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace Codeception\Stub;
use PHPUnit\Framework\MockObject\Rule\InvocationOrder;
/**
* Holds matcher and value of mocked method
*/
class StubMarshaler
{
private InvocationOrder $methodMatcher;
private $methodValue;
public function __construct(InvocationOrder $matcher, $value)
{
$this->methodMatcher = $matcher;
$this->methodValue = $value;
}
public function getMatcher(): InvocationOrder
{
return $this->methodMatcher;
}
public function getValue()
{
return $this->methodValue;
}
}

View File

@ -0,0 +1,299 @@
<?php
declare(strict_types=1);
namespace Codeception\Test\Feature;
use Exception;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use RuntimeException;
/**
* ### Usage in Codeception
*
* Since Codeception 2.3.8 this trait is enabled in `\Codeception\Test\Unit` class.
*
* ### Usage in PHPUnit
*
* Include this trait into a TestCase to be able to use Stubs and Mocks:
*
* ```php
* <?php
* class MyTest extends \PHPUnit\Framework\TestCase
* {
* use Codeception\Test\Feature\Stub;
* }
* ```
*/
trait Stub
{
private ?array $mocks = null;
protected function stubStart()
{
if ($this instanceof TestCase) {
return;
}
$this->mocks = [];
}
protected function stubEnd($status, $time)
{
if ($this instanceof TestCase) {
return;
}
if ($status !== 'ok') { // Codeception status
return;
}
foreach ($this->mocks as $mockObject) {
if ($mockObject->__phpunit_hasMatchers()) {
$this->assertTrue(true); // incrementing assertions
}
$mockObject->__phpunit_verify(true);
}
}
/**
* Instantiates a class without executing a constructor.
* Properties and methods can be set as a second parameter.
* Even protected and private properties can be set.
*
* ``` php
* <?php
* $this->make('User');
* $this->make('User', ['name' => 'davert']);
* ```
*
* Accepts either name of class or object of that class
*
* ``` php
* <?php
* $this->make(new User, ['name' => 'davert']);
* ```
*
* To replace method provide it's name as a key in second parameter
* and it's return value or callback function as parameter
*
* ``` php
* <?php
* $this->make('User', ['save' => function () { return true; }]);
* $this->make('User', ['save' => true]);
* ```
* @template RealInstanceType of object
* @param class-string<RealInstanceType>|RealInstanceType|callable(): class-string<RealInstanceType> $class - A class to be mocked
* @param array $params - properties and methods to set
*
* @return MockObject&RealInstanceType - mock
* @throws RuntimeException when class does not exist
* @throws Exception
*/
public function make($class, array $params = [])
{
return $this->mocks[] = \Codeception\Stub::make($class, $params, $this);
}
/**
* Instantiates class having all methods replaced with dummies.
* Constructor is not triggered.
* Properties and methods can be set as a second parameter.
* Even protected and private properties can be set.
*
* ``` php
* <?php
* $this->makeEmpty('User');
* $this->makeEmpty('User', ['name' => 'davert']);
* ```
*
* Accepts either name of class or object of that class
*
* ``` php
* <?php
* $this->makeEmpty(new User, ['name' => 'davert']);
* ```
*
* To replace method provide it's name as a key in second parameter
* and it's return value or callback function as parameter
*
* ``` php
* <?php
* $this->makeEmpty('User', ['save' => function () { return true; }]);
* $this->makeEmpty('User', ['save' => true]);
* ```
*
* @template RealInstanceType of object
* @param class-string<RealInstanceType>|RealInstanceType|callable(): class-string<RealInstanceType> $class - A class to be mocked
* @param array $params
* @return MockObject&RealInstanceType
* @throws Exception
*/
public function makeEmpty($class, array $params = [])
{
return $this->mocks[] = \Codeception\Stub::makeEmpty($class, $params, $this);
}
/**
* Instantiates class having all methods replaced with dummies except one.
* Constructor is not triggered.
* Properties and methods can be replaced.
* Even protected and private properties can be set.
*
* ``` php
* <?php
* $this->makeEmptyExcept('User', 'save');
* $this->makeEmptyExcept('User', 'save', ['name' => 'davert']);
* ```
*
* Accepts either name of class or object of that class
*
* ``` php
* <?php
* * $this->makeEmptyExcept(new User, 'save');
* ```
*
* To replace method provide it's name as a key in second parameter
* and it's return value or callback function as parameter
*
* ``` php
* <?php
* $this->makeEmptyExcept('User', 'save', ['isValid' => function () { return true; }]);
* $this->makeEmptyExcept('User', 'save', ['isValid' => true]);
* ```
*
* @template RealInstanceType of object
* @param class-string<RealInstanceType>|RealInstanceType|callable(): class-string<RealInstanceType> $class - A class to be mocked
*
* @return \PHPUnit\Framework\MockObject\MockObject&RealInstanceType
* @throws Exception
*/
public function makeEmptyExcept($class, string $method, array $params = [])
{
return $this->mocks[] = \Codeception\Stub::makeEmptyExcept($class, $method, $params, $this);
}
/**
* Instantiates a class instance by running constructor.
* Parameters for constructor passed as second argument
* Properties and methods can be set in third argument.
* Even protected and private properties can be set.
*
* ``` php
* <?php
* $this->construct('User', ['autosave' => false]);
* $this->construct('User', ['autosave' => false], ['name' => 'davert']);
* ```
*
* Accepts either name of class or object of that class
*
* ``` php
* <?php
* $this->construct(new User, ['autosave' => false), ['name' => 'davert']);
* ```
*
* To replace method provide it's name as a key in third parameter
* and it's return value or callback function as parameter
*
* ``` php
* <?php
* $this->construct('User', [], ['save' => function () { return true; }]);
* $this->construct('User', [], ['save' => true]);
* ```
*
* @template RealInstanceType of object
* @param class-string<RealInstanceType>|RealInstanceType|callable(): class-string<RealInstanceType> $class - A class to be mocked
* @return MockObject&RealInstanceType
* @throws Exception
*/
public function construct($class, array $constructorParams = [], array $params = [])
{
return $this->mocks[] = \Codeception\Stub::construct($class, $constructorParams, $params, $this);
}
/**
* Instantiates a class instance by running constructor with all methods replaced with dummies.
* Parameters for constructor passed as second argument
* Properties and methods can be set in third argument.
* Even protected and private properties can be set.
*
* ``` php
* <?php
* $this->constructEmpty('User', ['autosave' => false]);
* $this->constructEmpty('User', ['autosave' => false), ['name' => 'davert']);
* ```
*
* Accepts either name of class or object of that class
*
* ``` php
* <?php
* $this->constructEmpty(new User, ['autosave' => false], ['name' => 'davert']);
* ```
*
* To replace method provide it's name as a key in third parameter
* and it's return value or callback function as parameter
*
* ``` php
* <?php
* $this->constructEmpty('User', array(), array('save' => function () { return true; }));
* $this->constructEmpty('User', array(), array('save' => true));
* ```
*
* **To create a mock, pass current testcase name as last argument:**
*
* ```php
* <?php
* $this->constructEmpty('User', [], [
* 'save' => \Codeception\Stub\Expected::once()
* ]);
* ```
*
* @template RealInstanceType of object
* @param class-string<RealInstanceType>|RealInstanceType|callable(): class-string<RealInstanceType> $class - A class to be mocked
* @return MockObject&RealInstanceType
*/
public function constructEmpty($class, array $constructorParams = [], array $params = [])
{
return $this->mocks[] = \Codeception\Stub::constructEmpty($class, $constructorParams, $params, $this);
}
/**
* Instantiates a class instance by running constructor with all methods replaced with dummies, except one.
* Parameters for constructor passed as second argument
* Properties and methods can be set in third argument.
* Even protected and private properties can be set.
*
* ``` php
* <?php
* $this->constructEmptyExcept('User', 'save');
* $this->constructEmptyExcept('User', 'save', ['autosave' => false], ['name' => 'davert']);
* ```
*
* Accepts either name of class or object of that class
*
* ``` php
* <?php
* $this->constructEmptyExcept(new User, 'save', ['autosave' => false], ['name' => 'davert']);
* ```
*
* To replace method provide it's name as a key in third parameter
* and it's return value or callback function as parameter
*
* ``` php
* <?php
* $this->constructEmptyExcept('User', 'save', [], ['save' => function () { return true; }]);
* $this->constructEmptyExcept('User', 'save', [], ['save' => true]);
* ```
*
* @template RealInstanceType of object
* @param class-string<RealInstanceType>|RealInstanceType|callable(): class-string<RealInstanceType> $class - A class to be mocked
* @return MockObject&RealInstanceType
* @throws \ReflectionException
*/
public function constructEmptyExcept($class, string $method, array $constructorParams = [], array $params = [])
{
return $this->mocks[] = \Codeception\Stub::constructEmptyExcept($class, $method, $constructorParams, $params, $this);
}
}

View File

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
trait ResetMocks
{
protected function resetMockObjects()
{
$refl = new ReflectionObject($this);
while (!$refl->hasProperty('mockObjects')) {
$refl = $refl->getParentClass();
}
$prop = $refl->getProperty('mockObjects');
$prop->setValue($this, array());
}
}

View File

@ -0,0 +1,449 @@
<?php
declare(strict_types=1);
require_once __DIR__ .'/ResetMocks.php';
use Codeception\Stub;
use Codeception\Stub\StubMarshaler;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\MockObject\NoMoreReturnValuesConfiguredException;
use PHPUnit\Framework\TestCase;
use PHPUnit\Runner\Version as PHPUnitVersion;
final class StubTest extends TestCase
{
use ResetMocks;
protected DummyClass $dummy;
public function setUp(): void
{
require_once $file = __DIR__. '/_data/DummyAbstractClass.php';
require_once $file = __DIR__. '/_data/DummyOverloadableClass.php';
require_once $file = __DIR__. '/_data/DummyClass.php';
$this->dummy = new DummyClass(true);
}
public function testMakeEmpty()
{
$dummy = Stub::makeEmpty('DummyClass');
$this->assertInstanceOf('DummyClass', $dummy);
$this->assertTrue(method_exists($dummy, 'helloWorld'));
$this->assertNull($dummy->helloWorld());
}
public function testMakeEmptyMethodReplaced()
{
$dummy = Stub::makeEmpty('DummyClass', ['helloWorld' => fn(): string => 'good bye world']);
$this->assertMethodReplaced($dummy);
}
public function testMakeEmptyMethodSimplyReplaced()
{
$dummy = Stub::makeEmpty('DummyClass', ['helloWorld' => 'good bye world']);
$this->assertMethodReplaced($dummy);
}
public function testMakeEmptyExcept()
{
$dummy = Stub::makeEmptyExcept('DummyClass', 'helloWorld');
$this->assertEquals($this->dummy->helloWorld(), $dummy->helloWorld());
$this->assertNull($dummy->goodByeWorld());
}
public function testMakeEmptyExceptPropertyReplaced()
{
$dummy = Stub::makeEmptyExcept('DummyClass', 'getCheckMe', ['checkMe' => 'checked!']);
$this->assertEquals('checked!', $dummy->getCheckMe());
}
public function testMakeEmptyExceptMagicalPropertyReplaced()
{
$dummy = Stub::makeEmptyExcept('DummyClass', 'getCheckMeToo', ['checkMeToo' => 'checked!']);
$this->assertEquals('checked!', $dummy->getCheckMeToo());
}
public function testFactory()
{
$dummies = Stub::factory('DummyClass', 2);
$this->assertCount(2, $dummies);
$this->assertInstanceOf('DummyClass', $dummies[0]);
}
public function testMake()
{
$dummy = Stub::make('DummyClass', ['goodByeWorld' => fn(): string => 'hello world']);
$this->assertEquals($this->dummy->helloWorld(), $dummy->helloWorld());
$this->assertEquals("hello world", $dummy->goodByeWorld());
}
public function testMakeMethodReplaced()
{
$dummy = Stub::make('DummyClass', ['helloWorld' => fn(): string => 'good bye world']);
$this->assertMethodReplaced($dummy);
}
public function testMakeWithMagicalPropertiesReplaced()
{
$dummy = Stub::make('DummyClass', ['checkMeToo' => 'checked!']);
$this->assertEquals('checked!', $dummy->checkMeToo);
}
public function testMakeMethodSimplyReplaced()
{
$dummy = Stub::make('DummyClass', ['helloWorld' => 'good bye world']);
$this->assertMethodReplaced($dummy);
}
public function testCopy()
{
$dummy = Stub::copy($this->dummy, ['checkMe' => 'checked!']);
$this->assertEquals('checked!', $dummy->getCheckMe());
$dummy = Stub::copy($this->dummy, ['checkMeToo' => 'checked!']);
$this->assertEquals('checked!', $dummy->getCheckMeToo());
}
public function testConstruct()
{
$dummy = Stub::construct('DummyClass', ['checkMe' => 'checked!']);
$this->assertEquals('constructed: checked!', $dummy->getCheckMe());
$dummy = Stub::construct(
'DummyClass',
['checkMe' => 'checked!'],
['targetMethod' => fn(): bool => false]
);
$this->assertEquals('constructed: checked!', $dummy->getCheckMe());
$this->assertEquals(false, $dummy->targetMethod());
}
public function testConstructMethodReplaced()
{
$dummy = Stub::construct(
'DummyClass',
[],
['helloWorld' => fn(): string => 'good bye world']
);
$this->assertMethodReplaced($dummy);
}
public function testConstructMethodSimplyReplaced()
{
$dummy = Stub::make('DummyClass', ['helloWorld' => 'good bye world']);
$this->assertMethodReplaced($dummy);
}
public function testConstructEmpty()
{
$dummy = Stub::constructEmpty('DummyClass', ['checkMe' => 'checked!']);
$this->assertNull($dummy->getCheckMe());
}
public function testConstructEmptyExcept()
{
$dummy = Stub::constructEmptyExcept('DummyClass', 'getCheckMe', ['checkMe' => 'checked!']);
$this->assertNull($dummy->targetMethod());
$this->assertEquals('constructed: checked!', $dummy->getCheckMe());
}
public function testUpdate()
{
$dummy = Stub::construct('DummyClass');
Stub::update($dummy, ['checkMe' => 'done']);
$this->assertEquals('done', $dummy->getCheckMe());
Stub::update($dummy, ['checkMeToo' => 'done']);
$this->assertEquals('done', $dummy->getCheckMeToo());
}
public function testStubsFromObject()
{
$dummy = Stub::make(new DummyClass());
$this->assertInstanceOf(
MockObject::class,
$dummy
);
$dummy = Stub::make(new DummyOverloadableClass());
$this->assertSame(DummyOverloadableClass::class, get_parent_class($dummy));
$dummy = Stub::makeEmpty(new DummyClass());
$this->assertInstanceOf(
MockObject::class,
$dummy
);
$dummy = Stub::makeEmpty(new DummyOverloadableClass());
$this->assertSame(DummyOverloadableClass::class, get_parent_class($dummy));
$dummy = Stub::makeEmptyExcept(new DummyClass(), 'helloWorld');
$this->assertInstanceOf(
MockObject::class,
$dummy
);
$dummy = Stub::makeEmptyExcept(new DummyOverloadableClass(), 'helloWorld');
$this->assertSame(DummyOverloadableClass::class, get_parent_class($dummy));
$dummy = Stub::construct(new DummyClass());
$this->assertInstanceOf(
MockObject::class,
$dummy
);
$dummy = Stub::construct(new DummyOverloadableClass());
$this->assertSame(DummyOverloadableClass::class, get_parent_class($dummy));
$dummy = Stub::constructEmpty(new DummyClass());
$this->assertInstanceOf(
MockObject::class,
$dummy
);
$dummy = Stub::constructEmpty(new DummyOverloadableClass());
$this->assertSame(DummyOverloadableClass::class, get_parent_class($dummy));
$dummy = Stub::constructEmptyExcept(new DummyClass(), 'helloWorld');
$this->assertInstanceOf(
MockObject::class,
$dummy
);
$dummy = Stub::constructEmptyExcept(new DummyOverloadableClass(), 'helloWorld');
$this->assertSame(DummyOverloadableClass::class, get_parent_class($dummy));
}
protected function assertMethodReplaced($dummy)
{
$this->assertTrue(method_exists($dummy, 'helloWorld'));
$this->assertNotEquals($this->dummy->helloWorld(), $dummy->helloWorld());
$this->assertEquals($dummy->helloWorld(), 'good bye world');
}
/**
* @return array<int, array<string|StubMarshaler>>
*/
public static function matcherAndFailMessageProvider(): array
{
return [
[Stub\Expected::atLeastOnce(),
'Expected invocation at least once but it never'
],
[Stub\Expected::once(),
'Method was expected to be called 1 times, actually called 0 times.'
],
[Stub\Expected::exactly(1),
'Method was expected to be called 1 times, actually called 0 times.'
],
[Stub\Expected::exactly(3),
'Method was expected to be called 3 times, actually called 0 times.'
],
];
}
/**
* @dataProvider matcherAndFailMessageProvider
*/
#[DataProvider('matcherAndFailMessageProvider')]
public function testExpectedMethodIsCalledFail(StubMarshaler $stubMarshaler, string $failMessage)
{
$mock = Stub::makeEmptyExcept('DummyClass', 'call', ['targetMethod' => $stubMarshaler], $this);
$mock->goodByeWorld();
try {
$mock->__phpunit_verify();
$this->fail('Expected exception');
} catch (Exception $exception) {
$this->assertTrue(strpos($failMessage, $exception->getMessage()) >= 0, 'String contains');
}
$this->resetMockObjects();
}
public function testNeverExpectedMethodIsCalledFail()
{
$mock = Stub::makeEmptyExcept('DummyClass', 'call', ['targetMethod' => Stub\Expected::never()], $this);
$mock->goodByeWorld();
try {
$mock->call();
} catch (Exception $e) {
$this->assertTrue(strpos('was not expected to be called', $e->getMessage()) >= 0, 'String contains');
}
$this->resetMockObjects();
}
/**
* @return array<int, array<int|bool|StubMarshaler|string|null>>
*/
public static function matcherProvider(): array
{
return [
[0, Stub\Expected::never()],
[1, Stub\Expected::once()],
[2, Stub\Expected::atLeastOnce()],
[3, Stub\Expected::exactly(3)],
[1, Stub\Expected::once(fn(): bool => true)],
[2, Stub\Expected::atLeastOnce(fn(): array => [])],
[1, Stub\Expected::exactly(1, fn() => null)],
[1, Stub\Expected::exactly(1, fn(): string => 'hello world!')],
[1, Stub\Expected::exactly(1, 'hello world!')],
];
}
/**
* @dataProvider matcherProvider
*/
#[DataProvider('matcherProvider')]
public function testMethodMatcherWithMake(int $count, StubMarshaler $matcher, $expected = false)
{
$dummy = Stub::make('DummyClass', ['goodByeWorld' => $matcher], $this);
$this->repeatCall($count, [$dummy, 'goodByeWorld'], $expected);
}
/**
* @dataProvider matcherProvider
*/
#[DataProvider('matcherProvider')]
public function testMethodMatcherWithMakeEmpty(int $count, StubMarshaler $matcher)
{
$dummy = Stub::makeEmpty('DummyClass', ['goodByeWorld' => $matcher], $this);
$this->repeatCall($count, [$dummy, 'goodByeWorld']);
}
/**
* @dataProvider matcherProvider
*/
#[DataProvider('matcherProvider')]
public function testMethodMatcherWithMakeEmptyExcept(int $count, StubMarshaler $matcher)
{
$dummy = Stub::makeEmptyExcept('DummyClass', 'getCheckMe', ['goodByeWorld' => $matcher], $this);
$this->repeatCall($count, [$dummy, 'goodByeWorld']);
}
/**
* @dataProvider matcherProvider
*/
#[DataProvider('matcherProvider')]
public function testMethodMatcherWithConstruct(int $count, StubMarshaler $matcher)
{
$dummy = Stub::construct('DummyClass', [], ['goodByeWorld' => $matcher], $this);
$this->repeatCall($count, [$dummy, 'goodByeWorld']);
}
/**
* @dataProvider matcherProvider
*/
#[DataProvider('matcherProvider')]
public function testMethodMatcherWithConstructEmpty(int $count, StubMarshaler $matcher)
{
$dummy = Stub::constructEmpty('DummyClass', [], ['goodByeWorld' => $matcher], $this);
$this->repeatCall($count, [$dummy, 'goodByeWorld']);
}
/**
* @dataProvider matcherProvider
*/
#[DataProvider('matcherProvider')]
public function testMethodMatcherWithConstructEmptyExcept(int $count, StubMarshaler $matcher)
{
$dummy = Stub::constructEmptyExcept(
'DummyClass',
'getCheckMe',
[],
['goodByeWorld' => $matcher],
$this
);
$this->repeatCall($count, [$dummy, 'goodByeWorld']);
}
private function repeatCall($count, $callable, $expected = false)
{
for ($i = 0; $i < $count; ++$i) {
$actual = call_user_func($callable);
if ($expected) {
$this->assertEquals($expected, $actual);
}
}
}
public function testConsecutive()
{
$dummy = Stub::make('DummyClass', ['helloWorld' => Stub::consecutive('david', 'emma', 'sam', 'amy')]);
$this->assertEquals('david', $dummy->helloWorld());
$this->assertEquals('emma', $dummy->helloWorld());
$this->assertEquals('sam', $dummy->helloWorld());
$this->assertEquals('amy', $dummy->helloWorld());
// Expected null value when no more values
// For PHP 10.5.30 or higher an exception is thrown
// https://github.com/sebastianbergmann/phpunit/commit/490879817a1417fd5fa1149a47b6f2f1b70ada6a
if (version_compare(PHPUnitVersion::id(), '10.5.30', '>=')) {
$this->expectException(NoMoreReturnValuesConfiguredException::class);
$dummy->helloWorld();
} else {
$this->assertNull($dummy->helloWorld());
}
}
public function testStubPrivateProperties()
{
$tester = Stub::construct(
'MyClassWithPrivateProperties',
['name' => 'gamma'],
[
'randomName' => 'chicken',
't' => 'ticky2',
'getRandomName' => fn(): string => "randomstuff"
]
);
$this->assertEquals('gamma', $tester->getName());
$this->assertEquals('randomstuff', $tester->getRandomName());
$this->assertEquals('ticky2', $tester->getT());
}
public function testStubMakeEmptyInterface()
{
$stub = Stub::makeEmpty(Countable::class, ['count' => 5]);
$this->assertEquals(5, $stub->count());
}
public function testStubMakeEmptyAbstractClass()
{
if (version_compare(PHPUnitVersion::id(), '12', '>=')) {
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('PHPUnit 12 or greater does not allow to mock abstract classes anymore');
}
$stub = Stub::make('DummyAbstractClass');
$this->assertInstanceOf('DummyAbstractClass', $stub);
}
}
class MyClassWithPrivateProperties
{
private string $name = '';
private string $randomName = 'gaia';
private string $t = 'ticky';
public function __construct(string $name)
{
$this->name = $name;
}
public function getName(): string
{
return $this->name;
}
public function getRandomName(): string
{
return $this->randomName;
}
public function getT(): string
{
return $this->t;
}
}

View File

@ -0,0 +1,79 @@
<?php
declare(strict_types=1);
use Codeception\Stub\Expected;
use Codeception\Test\Feature\Stub;
use PHPUnit\Framework\TestCase;
require_once __DIR__ .'/ResetMocks.php';
final class StubTraitTest extends TestCase
{
use ResetMocks;
use Stub;
protected DummyClass $dummy;
public function setUp(): void
{
require_once $file = __DIR__. '/_data/DummyOverloadableClass.php';
require_once $file = __DIR__. '/_data/DummyClass.php';
$this->dummy = new DummyClass(true);
}
public function testMakeStubs()
{
$this->dummy = $this->make('DummyClass', ['helloWorld' => 'bye']);
$this->assertEquals('bye', $this->dummy->helloWorld());
$this->assertEquals('good bye', $this->dummy->goodByeWorld());
$this->dummy = $this->makeEmpty('DummyClass', ['helloWorld' => 'bye']);
$this->assertEquals('bye', $this->dummy->helloWorld());
$this->assertNull($this->dummy->goodByeWorld());
$this->dummy = $this->makeEmptyExcept('DummyClass', 'goodByeWorld', ['helloWorld' => 'bye']);
$this->assertEquals('bye', $this->dummy->helloWorld());
$this->assertEquals('good bye', $this->dummy->goodByeWorld());
$this->assertNull($this->dummy->exceptionalMethod());
}
public function testConstructStubs()
{
$this->dummy = $this->construct('DummyClass', ['!'], ['helloWorld' => 'bye']);
$this->assertEquals('constructed: !', $this->dummy->getCheckMe());
$this->assertEquals('bye', $this->dummy->helloWorld());
$this->assertEquals('good bye', $this->dummy->goodByeWorld());
$this->dummy = $this->constructEmpty('DummyClass', ['!'], ['helloWorld' => 'bye']);
$this->assertNull($this->dummy->getCheckMe());
$this->assertEquals('bye', $this->dummy->helloWorld());
$this->assertNull($this->dummy->goodByeWorld());
$this->dummy = $this->constructEmptyExcept('DummyClass', 'getCheckMe', ['!'], ['helloWorld' => 'bye']);
$this->assertEquals('constructed: !', $this->dummy->getCheckMe());
$this->assertEquals('bye', $this->dummy->helloWorld());
$this->assertNull($this->dummy->goodByeWorld());
$this->assertNull($this->dummy->exceptionalMethod());
}
public function testMakeMocks()
{
$this->dummy = $this->make('DummyClass', [
'helloWorld' => Expected::once()
]);
$this->dummy->helloWorld();
try {
$this->dummy->helloWorld();
} catch (Exception $exception) {
$this->assertTrue(
strpos('was not expected to be called more than once', $exception->getMessage()) >= 0,
'String contains'
);
$this->resetMockObjects();
return;
}
$this->fail('No exception thrown');
}
}

View File

@ -0,0 +1,7 @@
<?php
declare(strict_types=1);
abstract class DummyAbstractClass
{
}

View File

@ -0,0 +1,94 @@
<?php
declare(strict_types=1);
class DummyClass
{
/**
* @var int|string
*/
protected $checkMe = 1;
protected array $properties = ['checkMeToo' => 1];
function __construct($checkMe = 1)
{
$this->checkMe = 'constructed: '.$checkMe;
}
/** @return string */
public function helloWorld()
{
return 'hello';
}
/** @return string */
public function goodByeWorld()
{
return 'good bye';
}
/** @return string */
protected function notYourBusinessWorld()
{
return 'goAway';
}
/** @return string */
public function getCheckMe()
{
return $this->checkMe;
}
public function getCheckMeToo()
{
return $this->checkMeToo;
}
/** @return bool */
public function call()
{
$this->targetMethod();
return true;
}
/** @return bool */
public function targetMethod()
{
return true;
}
/**
* @throws Exception
*/
public function exceptionalMethod()
{
throw new Exception('Catch it!');
}
public function __set($name, $value)
{
if ($this->isMagical($name)) {
$this->properties[$name] = $value;
}
}
public function __get($name)
{
if ($this->__isset($name)) {
return $this->properties[$name];
}
}
public function __isset($name)
{
return $this->isMagical($name) && isset($this->properties[$name]);
}
/** @return bool */
private function isMagical($name)
{
$reflectionClass = new ReflectionClass($this);
return !$reflectionClass->hasProperty($name);
}
}

View File

@ -0,0 +1,93 @@
<?php
declare(strict_types=1);
class DummyOverloadableClass
{
/**
* @var int|string
*/
protected $checkMe = 1;
protected array $properties = ['checkMeToo' => 1];
function __construct($checkMe = 1)
{
$this->checkMe = 'constructed: '.$checkMe;
}
/** @return string */
public function helloWorld()
{
return 'hello';
}
/** @return string */
public function goodByeWorld()
{
return 'good bye';
}
/** @return string */
protected function notYourBusinessWorld()
{
return 'goAway';
}
/** @return string */
public function getCheckMe()
{
return $this->checkMe;
}
public function getCheckMeToo()
{
return $this->checkMeToo;
}
/** @return bool */
public function call()
{
$this->targetMethod();
return true;
}
/** @return bool */
public function targetMethod()
{
return true;
}
/**
* @throws Exception
*/
public function exceptionalMethod()
{
throw new Exception('Catch it!');
}
public function __get($name)
{
//seeing as we're not implementing __set here, add check for __mocked
$return = null;
if ($name === '__mocked') {
$return = property_exists($this, '__mocked') && $this->__mocked !== null ? $this->__mocked : null;
} elseif ($this->__isset($name)) {
$return = $this->properties[$name];
}
return $return;
}
public function __isset($name)
{
return $this->isMagical($name) && isset($this->properties[$name]);
}
/** @return bool */
private function isMagical($name)
{
$reflectionClass = new ReflectionClass($this);
return !$reflectionClass->hasProperty($name);
}
}