├── .github └── workflows │ ├── coding-standards.yml │ ├── phpmd.yml │ ├── phpunit.yml │ └── test-flight.yml ├── .gitignore ├── LICENSE ├── README.md ├── classes ├── FunctionProphecy.php ├── PHPProphet.php ├── ReferencePreservingRevealer.php └── Revelation.php ├── composer.json ├── phpcs.xml ├── phpunit.xml └── tests ├── PHPProphetTest.php └── RegressionTest.php /.github/workflows/coding-standards.yml: -------------------------------------------------------------------------------- 1 | name: "Check Coding Standards" 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | coding-standards: 8 | name: "Check Coding Standards" 9 | 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | 16 | - name: Setup PHP 17 | uses: shivammathur/setup-php@v2 18 | with: 19 | php-version: "7.4" 20 | tools: composer:v2, cs2pr 21 | 22 | - name: Install dependencies 23 | run: composer install --no-interaction --no-progress 24 | 25 | - name: Run phpcs 26 | run: vendor/bin/phpcs -q --report=checkstyle | cs2pr 27 | -------------------------------------------------------------------------------- /.github/workflows/phpmd.yml: -------------------------------------------------------------------------------- 1 | name: "Check phpmd" 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | coding-standards: 8 | name: "Check phpmd" 9 | 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | 16 | - name: Setup PHP 17 | uses: shivammathur/setup-php@v2 18 | with: 19 | php-version: "7.4" 20 | tools: composer:v2 21 | 22 | - name: Install dependencies 23 | run: composer require phpmd/phpmd --no-interaction --no-progress 24 | 25 | - name: Run phpmd 26 | run: vendor/bin/phpmd classes/ text cleancode,codesize,controversial,design,naming,unusedcode 27 | -------------------------------------------------------------------------------- /.github/workflows/phpunit.yml: -------------------------------------------------------------------------------- 1 | name: "PHPUnit tests" 2 | 3 | on: 4 | pull_request: 5 | schedule: 6 | - cron: "0 0 * * *" 7 | 8 | concurrency: 9 | group: ${{ github.head_ref || 'cron' }} 10 | cancel-in-progress: true 11 | 12 | jobs: 13 | phpunit: 14 | name: PHPUnit tests on PHP ${{ matrix.php-version }} 15 | 16 | runs-on: ubuntu-latest 17 | 18 | strategy: 19 | matrix: 20 | php-version: 21 | - "7.2" 22 | - "7.3" 23 | - "7.4" 24 | - "8.0" 25 | - "8.1" 26 | - "8.2" 27 | - "8.3" 28 | - '8.4' 29 | 30 | steps: 31 | - name: Checkout 32 | uses: actions/checkout@v4 33 | 34 | - name: Install PHP 35 | uses: shivammathur/setup-php@v2 36 | with: 37 | coverage: "pcov" 38 | php-version: "${{ matrix.php-version }}" 39 | ini-values: memory_limit=-1 40 | tools: composer:v2 41 | 42 | - name: Cache dependencies 43 | uses: actions/cache@v3 44 | with: 45 | path: | 46 | ~/.composer/cache 47 | vendor 48 | key: "php-${{ matrix.php-version }}" 49 | restore-keys: "php-${{ matrix.php-version }}" 50 | 51 | - name: Install dependencies 52 | run: composer install --no-interaction --no-progress 53 | 54 | - name: Tests 55 | run: vendor/bin/phpunit 56 | -------------------------------------------------------------------------------- /.github/workflows/test-flight.yml: -------------------------------------------------------------------------------- 1 | name: "Check test-flight" 2 | 3 | on: 4 | pull_request: 5 | push: 6 | 7 | jobs: 8 | coding-standards: 9 | name: "Check test-flight" 10 | 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | 17 | - name: Setup PHP 18 | uses: shivammathur/setup-php@v2 19 | with: 20 | php-version: "7.4" 21 | tools: composer:v2 22 | ini-values: "zend.assertions=1" 23 | 24 | - name: Install dependencies 25 | run: composer require cundd/test-flight --no-interaction --no-progress 26 | 27 | - name: Run test-flight 28 | run: | 29 | vendor/bin/test-flight README.md 30 | vendor/bin/test-flight classes/ 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.phpunit.result.cache 2 | /composer.lock 3 | /vendor/ 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 2 | Version 2, December 2004 3 | 4 | Copyright (C) 2004 Sam Hocevar 5 | 6 | Everyone is permitted to copy and distribute verbatim or modified 7 | copies of this license document, and changing it is allowed as long 8 | as the name is changed. 9 | 10 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 11 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 12 | 13 | 0. You just DO WHAT THE FUCK YOU WANT TO. 14 | 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mock PHP built-in functions with Prophecy 2 | 3 | This package integrates the function mock library 4 | [PHP-Mock](https://github.com/php-mock/php-mock) with Prophecy. 5 | 6 | # Installation 7 | 8 | Use [Composer](https://getcomposer.org/): 9 | 10 | ```sh 11 | composer require --dev php-mock/php-mock-prophecy 12 | ``` 13 | 14 | # Usage 15 | 16 | Build a new [`PHPProphet`](http://php-mock.github.io/php-mock-prophecy/api/class-phpmock.prophecy.PHPProphet.html) 17 | and create function prophecies for a given namespace 18 | with [`PHPProphet::prophesize()`](http://php-mock.github.io/php-mock-prophecy/api/class-phpmock.prophecy.PHPProphet.html#_prophesize): 19 | 20 | ```php 21 | namespace foo; 22 | 23 | use phpmock\prophecy\PHPProphet; 24 | 25 | $prophet = new PHPProphet(); 26 | 27 | $prophecy = $prophet->prophesize(__NAMESPACE__); 28 | $prophecy->time()->willReturn(123); 29 | $prophecy->reveal(); 30 | 31 | assert(123 == time()); 32 | $prophet->checkPredictions(); 33 | ``` 34 | 35 | ## Restrictions 36 | 37 | This library comes with the same restrictions as the underlying 38 | [`php-mock`](https://github.com/php-mock/php-mock#requirements-and-restrictions): 39 | 40 | * Only *unqualified* function calls in a namespace context can be prophesized. 41 | E.g. a call for `time()` in the namespace `foo` is prophesizable, 42 | a call for `\time()` is not. 43 | 44 | * The mock has to be defined before the first call to the unqualified function 45 | in the tested class. This is documented in [Bug #68541](https://bugs.php.net/bug.php?id=68541). 46 | In most cases you can ignore this restriction. But if you happen to run into 47 | this issue you can call [`PHPProphet::define()`](http://php-mock.github.io/php-mock-prophecy/api/class-phpmock.prophecy.PHPProphet.html#_define) 48 | before that first call. This would define a side effectless namespaced function. 49 | 50 | * Additionally it shares restrictions from Prophecy as well: 51 | Prophecy [doesn't support pass-by-reference](https://github.com/phpspec/prophecy/issues/225). 52 | If you need pass-by-reference in prophecies, consider using another framework 53 | (e.g. [php-mock-phpunit](https://github.com/php-mock/php-mock-phpunit)). 54 | 55 | # License and authors 56 | 57 | This project is free and under the WTFPL. 58 | Responsable for this project is Markus Malkusch markus@malkusch.de. 59 | 60 | ## Donations 61 | 62 | If you like this project and feel generous donate a few Bitcoins here: 63 | [1335STSwu9hST4vcMRppEPgENMHD2r1REK](bitcoin:1335STSwu9hST4vcMRppEPgENMHD2r1REK) 64 | -------------------------------------------------------------------------------- /classes/FunctionProphecy.php: -------------------------------------------------------------------------------- 1 | 14 | * @link bitcoin:1335STSwu9hST4vcMRppEPgENMHD2r1REK Donations 15 | * @license http://www.wtfpl.net/txt/copying/ WTFPL 16 | * @internal 17 | */ 18 | final class FunctionProphecy implements ProphecyInterface 19 | { 20 | /** 21 | * @var Prophet The prophet. 22 | */ 23 | private $prophet; 24 | 25 | /** 26 | * @var string The namespace. 27 | */ 28 | private $namespace; 29 | 30 | /** 31 | * @var Revelation[] The delegated prophecies. 32 | */ 33 | private $revelations = []; 34 | 35 | /** 36 | * Sets the prophet. 37 | * 38 | * @param string $namespace function namespace 39 | * @param Prophet $prophet prophet 40 | */ 41 | public function __construct($namespace, Prophet $prophet) 42 | { 43 | $this->prophet = $prophet; 44 | $this->namespace = $namespace; 45 | } 46 | 47 | /** 48 | * Creates a new function prophecy using the specified function name 49 | * and arguments. 50 | * 51 | * @param string $functionName function name 52 | * @param array $arguments arguments 53 | * 54 | * @return MethodProphecy function prophecy 55 | */ 56 | public function __call($functionName, array $arguments) 57 | { 58 | foreach ($this->revelations as $revelation) { 59 | if ( 60 | $revelation->namespace === $this->namespace 61 | && $revelation->functionName === $functionName 62 | ) { 63 | $prophecy = $revelation->prophecy; 64 | break; 65 | } 66 | } 67 | 68 | if (! isset($prophecy)) { 69 | $delegateBuilder = new MockDelegateFunctionBuilder(); 70 | $delegateBuilder->build($functionName); 71 | $prophecy = $this->prophet->prophesize($delegateBuilder->getFullyQualifiedClassName()); 72 | $this->revelations[] = new Revelation($this->namespace, $functionName, $prophecy); 73 | } 74 | 75 | return $prophecy->__call(MockDelegateFunctionBuilder::METHOD, $arguments); 76 | } 77 | 78 | /** 79 | * Reveals the function prophecies. 80 | * 81 | * I.e. the prophesized functions will become effective. 82 | */ 83 | public function reveal() 84 | { 85 | foreach ($this->revelations as $revelation) { 86 | $revelation->reveal(); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /classes/PHPProphet.php: -------------------------------------------------------------------------------- 1 | 17 | * namespace foo; 18 | * 19 | * use phpmock\prophecy\PHPProphet; 20 | * 21 | * $prophet = new PHPProphet(); 22 | * 23 | * $prophecy = $prophet->prophesize(__NAMESPACE__); 24 | * $prophecy->time()->willReturn(123); 25 | * $prophecy->reveal(); 26 | * 27 | * assert(123 == time()); 28 | * $prophet->checkPredictions(); 29 | * 30 | * 31 | * @author Markus Malkusch 32 | * @link bitcoin:1335STSwu9hST4vcMRppEPgENMHD2r1REK Donations 33 | * @license http://www.wtfpl.net/txt/copying/ WTFPL 34 | */ 35 | final class PHPProphet 36 | { 37 | /** 38 | * @var Prophet The prophet. 39 | */ 40 | private $prophet; 41 | 42 | /** 43 | * Builds the prophet. 44 | * 45 | * @param Prophet|null $prophet optional proxied prophet 46 | */ 47 | public function __construct(Prophet $prophet = null) 48 | { 49 | if (is_null($prophet)) { 50 | $prophet = new Prophet(); 51 | } 52 | 53 | $revealer = new ReferencePreservingRevealer(self::getProperty($prophet, "revealer")); 54 | $util = self::getProperty($prophet, "util"); 55 | $this->prophet = new Prophet($prophet->getDoubler(), $revealer, $util); 56 | } 57 | 58 | /** 59 | * Creates a new function prophecy for a given namespace. 60 | * 61 | * @param string $namespace function namespace 62 | * 63 | * @return ProphecyInterface function prophecy 64 | */ 65 | public function prophesize($namespace) 66 | { 67 | return new FunctionProphecy($namespace, $this->prophet); 68 | } 69 | 70 | /** 71 | * Checks all predictions defined by prophecies of this Prophet. 72 | * 73 | * It will also disable all previously revealed function prophecies. 74 | * 75 | * @throws AggregateException If any prediction fails. 76 | * @SuppressWarnings(PHPMD) 77 | */ 78 | public function checkPredictions() 79 | { 80 | Mock::disableAll(); 81 | $this->prophet->checkPredictions(); 82 | } 83 | 84 | /** 85 | * Defines the function prophecy in the given namespace. 86 | * 87 | * In most cases you don't have to call this method. {@link prophesize()} 88 | * is doing this for you. But if the prophecy is defined after the first 89 | * call in the tested class, the tested class doesn't resolve to the prophecy. 90 | * This is documented in Bug #68541. You therefore have to define 91 | * the namespaced function before the first call. 92 | * 93 | * Defining the function has no side effects. If the function was 94 | * already defined this method does nothing. 95 | * 96 | * @param string $namespace function namespace 97 | * @param string $name function name 98 | * 99 | * @see prophesize() 100 | * @link https://bugs.php.net/bug.php?id=68541 Bug #68541 101 | */ 102 | public static function define($namespace, $name) 103 | { 104 | $builder = new MockBuilder(); 105 | $builder->setNamespace($namespace) 106 | ->setName($name) 107 | ->setFunction(function () { 108 | }) 109 | ->build() 110 | ->define(); 111 | } 112 | 113 | /** 114 | * Returns a private property of a prophet. 115 | * 116 | * @param Prophet $prophet prophet 117 | * @param string $property property name 118 | * 119 | * @return mixed property value of that prophet 120 | */ 121 | private static function getProperty(Prophet $prophet, $property) 122 | { 123 | $reflection = new ReflectionProperty($prophet, $property); 124 | $reflection->setAccessible(true); 125 | return $reflection->getValue($prophet); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /classes/ReferencePreservingRevealer.php: -------------------------------------------------------------------------------- 1 | 12 | * @link bitcoin:1335STSwu9hST4vcMRppEPgENMHD2r1REK Donations 13 | * @license http://www.wtfpl.net/txt/copying/ WTFPL 14 | * @internal 15 | */ 16 | final class ReferencePreservingRevealer implements RevealerInterface 17 | { 18 | /** 19 | * @var RevealerInterface The subject. 20 | */ 21 | private $revealer; 22 | 23 | /** 24 | * Sets the subject. 25 | * 26 | * @param RevealerInterface $revealer proxied revealer 27 | */ 28 | public function __construct(RevealerInterface $revealer) 29 | { 30 | $this->revealer = $revealer; 31 | } 32 | 33 | /** 34 | * @SuppressWarnings(PHPMD) 35 | */ 36 | public function reveal($value) 37 | { 38 | if (is_array($value)) { 39 | MockFunctionGenerator::removeDefaultArguments($value); 40 | foreach ($value as &$item) { 41 | $item = $this->revealer->reveal($item); 42 | } 43 | return $value; 44 | } else { 45 | return $this->revealer->reveal($value); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /classes/Revelation.php: -------------------------------------------------------------------------------- 1 | 13 | * @link bitcoin:1335STSwu9hST4vcMRppEPgENMHD2r1REK Donations 14 | * @license http://www.wtfpl.net/txt/copying/ WTFPL 15 | * @internal 16 | */ 17 | final class Revelation implements ProphecyInterface 18 | { 19 | /** 20 | * @internal 21 | * @var string The function namespace. 22 | */ 23 | public $namespace; 24 | 25 | /** 26 | * @internal 27 | * @var string The function name. 28 | */ 29 | public $functionName; 30 | 31 | /** 32 | * @internal 33 | * @var ProphecyInterface The prophecy. 34 | */ 35 | public $prophecy; 36 | 37 | /** 38 | * Builds the revelation. 39 | * 40 | * @param String $namespace function namespace 41 | * @param String $functionName function name 42 | * @param ProphecyInterface $prophecy prophecy 43 | */ 44 | public function __construct($namespace, $functionName, ProphecyInterface $prophecy) 45 | { 46 | $this->namespace = $namespace; 47 | $this->functionName = $functionName; 48 | $this->prophecy = $prophecy; 49 | } 50 | 51 | /** 52 | * Reveals the function prophecy. 53 | * 54 | * I.e. the prophesized function will become effective. 55 | * 56 | * @return Mock enabled function mock 57 | */ 58 | public function reveal() 59 | { 60 | $delegate = $this->prophecy->reveal(); 61 | $builder = new MockBuilder(); 62 | $builder->setNamespace($this->namespace) 63 | ->setName($this->functionName) 64 | ->setFunction([$delegate, MockDelegateFunctionBuilder::METHOD]); 65 | $mock = $builder->build(); 66 | $mock->enable(); 67 | return $mock; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "php-mock/php-mock-prophecy", 3 | "type": "library", 4 | "description": "Mock built-in PHP functions (e.g. time()) with Prophecy. This package relies on PHP's namespace fallback policy. No further extension is needed.", 5 | "keywords": ["prophecy", "mock", "stub", "test double", "function", "test", "testing", "TDD", "BDD"], 6 | "homepage": "https://github.com/php-mock/php-mock-prophecy", 7 | "license": "WTFPL", 8 | "authors": [ 9 | { 10 | "name": "Markus Malkusch", 11 | "email": "markus@malkusch.de", 12 | "homepage": "http://markus.malkusch.de", 13 | "role": "Developer" 14 | } 15 | ], 16 | "autoload": { 17 | "psr-4": { 18 | "phpmock\\prophecy\\": "classes/" 19 | } 20 | }, 21 | "require": { 22 | "php": ">=7.2", 23 | "php-mock/php-mock-integration": "^2.2.1 || ^3.0", 24 | "phpspec/prophecy": "^1.12.1" 25 | }, 26 | "require-dev": { 27 | "phpunit/phpunit": "^8.5.13 || ^9.5", 28 | "squizlabs/php_codesniffer": "^3.8" 29 | }, 30 | "archive": { 31 | "exclude": ["/tests"] 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | classes 9 | tests 10 | 11 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | tests/ 7 | 8 | -------------------------------------------------------------------------------- /tests/PHPProphetTest.php: -------------------------------------------------------------------------------- 1 | 12 | * @link bitcoin:1335STSwu9hST4vcMRppEPgENMHD2r1REK Donations 13 | * @license http://www.wtfpl.net/txt/copying/ WTFPL 14 | * @see PHPProphet 15 | */ 16 | final class PHPProphetTest extends AbstractMockTestCase 17 | { 18 | /** 19 | * @var PHPProphet The SUT. 20 | */ 21 | private $prophet; 22 | 23 | protected function setUp(): void 24 | { 25 | parent::setUp(); 26 | 27 | $this->prophet = new PHPProphet(); 28 | } 29 | 30 | protected function disableMocks() 31 | { 32 | $this->prophet->checkPredictions(); 33 | } 34 | 35 | protected function mockFunction($namespace, $functionName, callable $function) 36 | { 37 | $prophecy = $this->prophet->prophesize($namespace); 38 | $prophecy->$functionName(Argument::cetera())->will(function (array $parameters) use ($function) { 39 | return call_user_func_array($function, $parameters); 40 | }); 41 | $prophecy->reveal(); 42 | } 43 | 44 | protected function defineFunction($namespace, $functionName) 45 | { 46 | PHPProphet::define($namespace, $functionName); 47 | } 48 | 49 | public function testDoubleMockTheSameFunctionWithDifferentArguments() 50 | { 51 | $prophecy = $this->prophet->prophesize(__NAMESPACE__); 52 | $prophecy->min(1, 10)->willReturn(0); 53 | $prophecy->min(20, 30)->willReturn(1); 54 | $prophecy->reveal(); 55 | 56 | $this->assertSame(0, min(1, 10)); 57 | $this->assertSame(1, min(20, 30)); 58 | } 59 | 60 | public function testTwoDifferentFunctionsMock() 61 | { 62 | $prophecy = $this->prophet->prophesize(__NAMESPACE__); 63 | $prophecy->min(1, 10)->willReturn(0); 64 | $prophecy->max(20, 30)->willReturn(1); 65 | $prophecy->reveal(); 66 | 67 | $this->assertSame(0, min(1, 10)); 68 | $this->assertSame(1, max(20, 30)); 69 | } 70 | 71 | /** 72 | * This test is skipped until PHPUnit#2016 is resolved. 73 | * 74 | * @see https://github.com/sebastianbergmann/phpunit/issues/2016 75 | */ 76 | public function testBackupStaticAttributes() 77 | { 78 | $this->markTestSkipped("Skip until PHPUnit#2016 is resolved"); 79 | } 80 | 81 | /** 82 | * Pass-By-Reference is not supported in Prophecy. 83 | * 84 | * @see https://github.com/phpspec/prophecy/issues/225 85 | */ 86 | public function testPassingByReference() 87 | { 88 | $this->markTestSkipped("Pass-By-Reference is not supported in Prophecy"); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /tests/RegressionTest.php: -------------------------------------------------------------------------------- 1 | 12 | * @link bitcoin:1335STSwu9hST4vcMRppEPgENMHD2r1REK Donations 13 | * @license http://www.wtfpl.net/txt/copying/ WTFPL 14 | */ 15 | final class RegressionTest extends TestCase 16 | { 17 | /** 18 | * Calling no optional parameter 19 | * 20 | * @test 21 | * @see https://github.com/php-mock/php-mock-prophecy/issues/1 22 | */ 23 | public function expectingWithoutOptionalParameter() 24 | { 25 | $prophet = new Prophet(); 26 | $prophecy = $prophet->prophesize(OptionalParameterHolder::class); 27 | $prophecy->call("arg1")->willReturn("mocked"); 28 | $mock = $prophecy->reveal(); 29 | 30 | $this->assertEquals("mocked", $mock->call("arg1")); 31 | $prophet->checkPredictions(); 32 | } 33 | } 34 | 35 | // @codingStandardsIgnoreStart 36 | class OptionalParameterHolder 37 | { 38 | 39 | public function call($arg1, $optional = "optional") 40 | { 41 | return $arg1 . $optional; 42 | } 43 | } 44 | // @codingStandardsIgnoreEnd 45 | --------------------------------------------------------------------------------