├── src ├── autoload.php ├── MockFinals.php └── Definaler.php ├── composer.json ├── LICENSE └── README.md /src/autoload.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | Awesomite\MockFinals\MockFinals::enable(); 15 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "awesomite/mock-finals", 3 | "description": "Mocking final classes and methods", 4 | "type": "library", 5 | "require": { 6 | "php": "^7.1", 7 | "ext-uopz": "*" 8 | }, 9 | "require-dev": { 10 | "phpunit/phpunit": "^7.5 || ^8.5 || ^9.2" 11 | }, 12 | "license": "MIT", 13 | "authors": [ 14 | { 15 | "name": "Bartłomiej Krukowski", 16 | "email": "bartlomiej@krukowski.me" 17 | } 18 | ], 19 | "autoload": { 20 | "files": ["src/autoload.php"], 21 | "psr-4": { 22 | "Awesomite\\MockFinals\\": "src/" 23 | } 24 | }, 25 | "autoload-dev": { 26 | "psr-4": { 27 | "Awesomite\\MockFinals\\": "tests/" 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Bartłomiej Krukowski 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://github.com/awesomite/mock-finals/workflows/Tests/badge.svg?branch=master)](https://github.com/awesomite/mock-finals/actions?query=workflow%3ATests) 2 | 3 | # Mock Finals 4 | 5 | Mock final classes and methods in your tests. Library overrides existing class loaders and removes all `final` 6 | occurrences in runtime using [`uopz_flags`](https://www.php.net/manual/en/function.uopz-flags.php). 7 | 8 | ## Installation 9 | 10 | ``` 11 | composer require --dev awesomite/mock-finals 12 | ``` 13 | 14 | ## Requirements 15 | 16 | * PHP ^7.1 17 | * [uopz](https://www.php.net/manual/en/book.uopz.php) (`pecl install uopz`) 18 | 19 | ## Use cases 20 | 21 | In general, it's a bad practice to do so. However it may be helpful when you have to deal with legacy code or third party libraries. 22 | Please read [the following article](https://ocramius.github.io/blog/when-to-declare-classes-final/) to understand how to properly deal with `final` keyword in your code. 23 | 24 | ## Example 25 | 26 | 27 | ```php 28 | class Greeter 29 | { 30 | final public function sayHello(): string 31 | { 32 | return 'hello'; 33 | } 34 | } 35 | 36 | class MyTest extends \PHPUnit\Framework\TestCase 37 | { 38 | public function testSayHello(): void 39 | { 40 | $mock = $this->getMockBuilder(Greeter::class)->getMock(); 41 | $mock 42 | ->expects($this->once()) 43 | ->method('sayHello') 44 | ->willReturn('goodbye') 45 | ; 46 | $this->assertSame('goodbye', $mock->sayHello()); 47 | } 48 | } 49 | ``` 50 | -------------------------------------------------------------------------------- /src/MockFinals.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Awesomite\MockFinals; 15 | 16 | /** 17 | * @internal 18 | */ 19 | final class MockFinals 20 | { 21 | /** 22 | * @var callable[] 23 | */ 24 | private static $loaders = []; 25 | 26 | public function __invoke(string $className): void 27 | { 28 | foreach (self::$loaders as $loader) { 29 | \call_user_func($loader, $className); 30 | if (\class_exists($className, false)) { 31 | Definaler::definal($className); 32 | 33 | break; 34 | } 35 | } 36 | } 37 | 38 | public static function enable(): void 39 | { 40 | foreach (\spl_autoload_functions() as $fn) { 41 | \spl_autoload_unregister($fn); 42 | self::$loaders[] = $fn; 43 | } 44 | 45 | \spl_autoload_register(new static(), true, true); 46 | self::definalExisting(); 47 | } 48 | 49 | private static function definalExisting(): void 50 | { 51 | foreach (\get_declared_classes() as $className) { 52 | $class = new \ReflectionClass($className); 53 | if (!$class->isInternal()) { 54 | Definaler::definal($className); 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Definaler.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Awesomite\MockFinals; 15 | 16 | /** 17 | * @internal 18 | */ 19 | final class Definaler 20 | { 21 | public static function definal(string $class): void 22 | { 23 | self::definalClass($class); 24 | 25 | foreach ((new \ReflectionClass($class))->getMethods(\ReflectionMethod::IS_FINAL) as $method) { 26 | self::definalMethod($class, $method->getName()); 27 | } 28 | } 29 | 30 | private static function definalClass(string $class): void 31 | { 32 | /* 33 | * It is required to add 34 | * "& ~(\ZEND_ACC_PUBLIC | \ZEND_ACC_PROTECTED | \ZEND_ACC_PRIVATE | \ZEND_ACC_STATIC)" 35 | * otherwise code can throw the following exceptions: 36 | * 37 | * - RuntimeException: attempt to set public, private or protected on class entry %s, not allowed in %s:%d 38 | * - RuntimeException: attempt to set static on class entry %s, not allowed in %s:%d 39 | */ 40 | \uopz_flags( 41 | $class, 42 | '', 43 | \uopz_flags($class, '') & ~\ZEND_ACC_FINAL & ~(\ZEND_ACC_PUBLIC | \ZEND_ACC_PROTECTED | \ZEND_ACC_PRIVATE | \ZEND_ACC_STATIC) 44 | ); 45 | } 46 | 47 | private static function definalMethod(string $class, string $function): void 48 | { 49 | \uopz_flags($class, $function, \uopz_flags($class, $function) & ~\ZEND_ACC_FINAL); 50 | } 51 | } 52 | --------------------------------------------------------------------------------