├── .gitattributes ├── .github └── workflows │ ├── ci-scheduled.yml │ ├── ci.yml │ └── publish-release.yml ├── .gitignore ├── .php-cs-fixer.php ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── composer.json ├── phpunit.xml └── src ├── Liberator.php ├── LiberatorArray.php ├── LiberatorClass.php ├── LiberatorObject.php └── LiberatorProxyInterface.php /.gitattributes: -------------------------------------------------------------------------------- 1 | /test/ export-ignore 2 | -------------------------------------------------------------------------------- /.github/workflows/ci-scheduled.yml: -------------------------------------------------------------------------------- 1 | name: CI (scheduled) 2 | on: 3 | schedule: 4 | - cron: 0 14 * * 0 # Sunday 2PM UTC = Monday 12AM AEST 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | php: ['7.4', '8.0', '8.1'] 12 | name: PHP ${{ matrix.php }} 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v1 16 | - name: Set up PHP 17 | uses: shivammathur/setup-php@v2 18 | with: 19 | php-version: ${{ matrix.php }} 20 | extensions: mbstring 21 | coverage: pcov 22 | - name: Check PHP version 23 | run: php -v 24 | - name: Install dependencies 25 | run: make vendor 26 | - name: Make 27 | if: ${{ startsWith(matrix.php, '7.') }} 28 | run: make ci 29 | - name: Make 30 | if: ${{ startsWith(matrix.php, '8.') }} 31 | run: make artifacts/coverage/phpunit/clover.xml 32 | - name: Publish coverage 33 | if: success() 34 | uses: codecov/codecov-action@v2 35 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | pull_request: 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | php: ['7.4', '8.0', '8.1'] 12 | name: PHP ${{ matrix.php }} 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v1 16 | - name: Set up PHP 17 | uses: shivammathur/setup-php@v2 18 | with: 19 | php-version: ${{ matrix.php }} 20 | extensions: mbstring 21 | coverage: pcov 22 | - name: Check PHP version 23 | run: php -v 24 | - name: Install dependencies 25 | run: make vendor 26 | - name: Make 27 | if: ${{ startsWith(matrix.php, '7.') }} 28 | run: make ci 29 | - name: Make 30 | if: ${{ startsWith(matrix.php, '8.') }} 31 | run: make artifacts/coverage/phpunit/clover.xml 32 | - name: Publish coverage 33 | if: success() 34 | uses: codecov/codecov-action@v2 35 | -------------------------------------------------------------------------------- /.github/workflows/publish-release.yml: -------------------------------------------------------------------------------- 1 | name: Publish release 2 | on: 3 | push: 4 | tags: 5 | - '*' 6 | jobs: 7 | publish: 8 | runs-on: ubuntu-latest 9 | name: Publish release 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v2 13 | - name: Publish release 14 | uses: eloquent/github-release-action@v1 15 | env: 16 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.makefiles/ 2 | /artifacts/ 3 | /composer.lock 4 | /vendor/ 5 | -------------------------------------------------------------------------------- /.php-cs-fixer.php: -------------------------------------------------------------------------------- 1 | setCacheFile(__DIR__ . '/artifacts/lint/php-cs-fixer/cache'); 5 | $config->getFinder()->exclude([ 6 | 'artifacts', 7 | ]); 8 | 9 | return $config; 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Liberator changelog 2 | 3 | ## 3.0.0 (2022-02-21) 4 | 5 | - **[BC BREAK]** Dropped support for EOL PHP versions including: 6 | - `5.3` 7 | - `5.4` 8 | - `5.5` 9 | - `5.6` 10 | - `7.0` 11 | - `7.1` 12 | - `7.2` 13 | - `7.3` 14 | - **[FIXED]** Fixed deprecation warnings under PHP `8.1`. 15 | 16 | ## 2.0.0 (2014-02-09) 17 | 18 | - **[BC BREAK]** Some class members that were previously protected are now 19 | private. It is very unlikely that this affects anyone at all, but technically 20 | it's backwards incompatible. 21 | - **[NEW]** Added an interface for identifying liberator proxied values. 22 | - **[NEW]** API documentation 23 | - **[MAINTENANCE]** Repository maintenance 24 | 25 | ## 1.1.1 (2013-03-04) 26 | 27 | - **[NEW]** Added [Archer] integration 28 | - **[MAINTENANCE]** Repository maintenance 29 | 30 | [Archer]: https://github.com/IcecaveStudios/archer 31 | 32 | ## 1.1.0 (2012-08-02) 33 | 34 | - **[IMPROVED]** Improved API 35 | 36 | ## 1.0.0 (2012-08-02) 37 | 38 | - **[NEW]** Initial stable release 39 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | As a guideline, please follow this process when contributing: 4 | 5 | 1. [Fork the repository] 6 | 2. [Create a branch] 7 | 3. Make your changes 8 | 4. Use `make prepare` to run tests and code style checks 9 | 5. [Squash commits] if necessary 10 | 6. [Create a pull request] 11 | 12 | [create a branch]: https://help.github.com/articles/about-branches 13 | [create a pull request]: https://help.github.com/articles/creating-a-pull-request 14 | [fork the repository]: https://help.github.com/articles/fork-a-repo 15 | [squash commits]: https://help.github.com/articles/about-git-rebase 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © 2022 Erin Millard 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 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Powered by https://makefiles.dev/ 2 | 3 | export PHP_ERROR_EXCEPTION_DEPRECATIONS=true 4 | 5 | ################################################################################ 6 | 7 | -include .makefiles/Makefile 8 | -include .makefiles/pkg/php/v1/Makefile 9 | 10 | .makefiles/%: 11 | @curl -sfL https://makefiles.dev/v1 | bash /dev/stdin "$@" 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > # No longer maintained 2 | > 3 | > This package is no longer maintained. See [this statement] for more info. 4 | > 5 | > [this statement]: https://gist.github.com/ezzatron/713a548735febe3d76f8ca831bc895c0 6 | 7 | # Liberator 8 | 9 | *A proxy for circumventing PHP access modifier restrictions.* 10 | 11 | ## Installation and documentation 12 | 13 | - Available as [Composer] package [eloquent/liberator]. 14 | 15 | ## What is Liberator? 16 | 17 | *Liberator* allows access to **protected** and **private** methods and 18 | properties of objects as if they were marked **public**. It can do so for both 19 | objects and classes (i.e. static methods and properties). 20 | 21 | *Liberator*'s primary use is as a testing tool, allowing direct access to 22 | methods that would otherwise require complicated test harnesses or mocking to 23 | test. 24 | 25 | ## Usage 26 | 27 | ### For objects 28 | 29 | Take the following class: 30 | 31 | ```php 32 | class SeriousBusiness 33 | { 34 | private function foo($adjective) 35 | { 36 | return 'foo is ' . $adjective; 37 | } 38 | 39 | private $bar = 'mind'; 40 | } 41 | ``` 42 | 43 | Normally there is no way to call `foo()` or access `$bar` from outside the 44 | `SeriousBusiness` class, but *Liberator* allows this to be achieved: 45 | 46 | ```php 47 | use Eloquent\Liberator\Liberator; 48 | 49 | $object = new SeriousBusiness; 50 | $liberator = Liberator::liberate($object); 51 | 52 | echo $liberator->foo('not so private...'); // outputs 'foo is not so private...' 53 | echo $liberator->bar . ' = blown'; // outputs 'mind = blown' 54 | ``` 55 | 56 | ### For classes 57 | 58 | The same concept applies for static methods and properties: 59 | 60 | ```php 61 | class SeriousBusiness 62 | { 63 | static private function baz($adjective) 64 | { 65 | return 'baz is ' . $adjective; 66 | } 67 | 68 | static private $qux = 'mind'; 69 | } 70 | ``` 71 | 72 | To access these, a *class liberator* must be used instead of an *object 73 | liberator*, but they operate in a similar manner: 74 | 75 | ```php 76 | use Eloquent\Liberator\Liberator; 77 | 78 | $liberator = Liberator::liberateClass('SeriousBusiness'); 79 | 80 | echo $liberator->baz('not so private...'); // outputs 'baz is not so private...' 81 | echo $liberator->qux . ' = blown'; // outputs 'mind = blown' 82 | ``` 83 | 84 | Alternatively, *Liberator* can generate a class that can be used statically: 85 | 86 | ```php 87 | use Eloquent\Liberator\Liberator; 88 | 89 | $liberatorClass = Liberator::liberateClassStatic('SeriousBusiness'); 90 | 91 | echo $liberatorClass::baz('not so private...'); // outputs 'baz is not so private...' 92 | echo $liberatorClass::liberator()->qux . ' = blown'; // outputs 'mind = blown' 93 | ``` 94 | 95 | Unfortunately, there is (currently) no __getStatic() or __setStatic() in PHP, 96 | so accessing static properties in this way is a not as elegant as it could be. 97 | 98 | ## Applications for Liberator 99 | 100 | - Writing [white-box] style unit tests (testing protected/private methods). 101 | - Modifying behavior of poorly designed third-party libraries. 102 | 103 | 104 | 105 | [white-box]: http://en.wikipedia.org/wiki/White-box_testing 106 | 107 | [composer]: http://getcomposer.org/ 108 | [eloquent/liberator]: https://packagist.org/packages/eloquent/liberator 109 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eloquent/liberator", 3 | "description": "A proxy for circumventing PHP access modifier restrictions.", 4 | "keywords": ["access", "modifier", "object", "proxy", "private", "protected", "reflection"], 5 | "homepage": "https://github.com/eloquent/liberator", 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Erin Millard", 10 | "email": "ezzatron@gmail.com", 11 | "homepage": "http://ezzatron.com/" 12 | } 13 | ], 14 | "require": { 15 | "php": "^7.4 || ^8", 16 | "eloquent/pops": "^5" 17 | }, 18 | "require-dev": { 19 | "eloquent/code-style": "^2", 20 | "errors/exceptions": "^0.2", 21 | "friendsofphp/php-cs-fixer": "^3", 22 | "phpunit/phpunit": "^9" 23 | }, 24 | "autoload": { 25 | "psr-4": { 26 | "Eloquent\\Liberator\\": "src" 27 | } 28 | }, 29 | "autoload-dev": { 30 | "psr-4": { 31 | "Eloquent\\Liberator\\": ["test/src"] 32 | }, 33 | "files": [ 34 | "test/src/SeriousBusiness.php" 35 | ] 36 | }, 37 | "extra": { 38 | "branch-alias": { 39 | "dev-main": "3.0.x-dev" 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | test/suite 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | src 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/Liberator.php: -------------------------------------------------------------------------------- 1 | popsCall($method, $arguments); 45 | } 46 | 47 | /** 48 | * Set the wrapped class. 49 | * 50 | * @param string $class The class to wrap. 51 | * 52 | * @throws InvalidTypeException If the supplied value is not the correct type. 53 | */ 54 | public function setPopsValue($class) 55 | { 56 | parent::setPopsValue($class); 57 | 58 | $this->liberatorReflector = new ReflectionClass($class); 59 | } 60 | 61 | /** 62 | * Call a static method on the proxied class with support for by-reference 63 | * arguments. 64 | * 65 | * @param string $method The name of the method to call. 66 | * @param array &$arguments The arguments. 67 | * 68 | * @return mixed The result of the method call. 69 | */ 70 | public function popsCall($method, array &$arguments) 71 | { 72 | if ($this->liberatorReflector()->hasMethod($method)) { 73 | $method = $this->liberatorReflector()->getMethod($method); 74 | $method->setAccessible(true); 75 | 76 | return static::popsProxySubValue( 77 | $method->invokeArgs(null, $arguments), 78 | $this->isPopsRecursive() 79 | ); 80 | } 81 | 82 | return parent::popsCall($method, $arguments); 83 | } 84 | 85 | /** 86 | * Set the value of a static property on the proxied class. 87 | * 88 | * @param string $property The name of the property to set. 89 | * @param mixed $value The new value. 90 | */ 91 | public function __set($property, $value) 92 | { 93 | if ($propertyReflector = $this->liberatorPropertyReflector($property)) { 94 | $propertyReflector->setValue(null, $value); 95 | 96 | return; 97 | } 98 | 99 | throw new LogicException( 100 | sprintf( 101 | 'Access to undeclared static property: %s::$%s', 102 | $this->popsValue(), 103 | $property 104 | ) 105 | ); 106 | } 107 | 108 | /** 109 | * Get the value of a static property on the proxied class. 110 | * 111 | * @param string $property The name of the property to get. 112 | * 113 | * @return mixed The value of the property. 114 | */ 115 | public function __get($property) 116 | { 117 | if ($propertyReflector = $this->liberatorPropertyReflector($property)) { 118 | return static::popsProxySubValue( 119 | $propertyReflector->getValue(null), 120 | $this->isPopsRecursive() 121 | ); 122 | } 123 | 124 | throw new LogicException( 125 | sprintf( 126 | 'Access to undeclared static property: %s::$%s', 127 | $this->popsValue(), 128 | $property 129 | ) 130 | ); 131 | } 132 | 133 | /** 134 | * Returns true if the supplied static property exists on the proxied class. 135 | * 136 | * @param string $property The name of the property to search for. 137 | * 138 | * @return bool True if the property exists. 139 | */ 140 | public function __isset($property) 141 | { 142 | if ($propertyReflector = $this->liberatorPropertyReflector($property)) { 143 | return null !== $propertyReflector->getValue(null); 144 | } 145 | 146 | return parent::__isset($property); 147 | } 148 | 149 | /** 150 | * Set the value of a static property on the proxied class to null. 151 | * 152 | * @param string $property The name of the property to set. 153 | */ 154 | public function __unset($property) 155 | { 156 | if ($propertyReflector = $this->liberatorPropertyReflector($property)) { 157 | $propertyReflector->setValue(null, null); 158 | 159 | return; 160 | } 161 | 162 | throw new LogicException( 163 | sprintf( 164 | 'Access to undeclared static property: %s::$%s', 165 | $this->popsValue(), 166 | $property 167 | ) 168 | ); 169 | } 170 | 171 | /** 172 | * Get the proxy class. 173 | * 174 | * @return string The proxy class. 175 | */ 176 | protected static function popsProxyClass() 177 | { 178 | return 'Eloquent\Liberator\Liberator'; 179 | } 180 | 181 | /** 182 | * Get the class reflector. 183 | * 184 | * @return ReflectionClass The class reflector. 185 | */ 186 | protected function liberatorReflector() 187 | { 188 | return $this->liberatorReflector; 189 | } 190 | 191 | /** 192 | * Get a property reflector. 193 | * 194 | * @param string $property The property name. 195 | * 196 | * @return ReflectionProperty|null The property reflector, or null if no such property exists. 197 | */ 198 | protected function liberatorPropertyReflector($property) 199 | { 200 | $classReflector = $this->liberatorReflector(); 201 | 202 | while ($classReflector) { 203 | if ($classReflector->hasProperty($property)) { 204 | $propertyReflector = $classReflector->getProperty($property); 205 | $propertyReflector->setAccessible(true); 206 | 207 | return $propertyReflector; 208 | } 209 | 210 | $classReflector = $classReflector->getParentClass(); 211 | } 212 | 213 | return null; 214 | } 215 | 216 | private $liberatorReflector; 217 | } 218 | -------------------------------------------------------------------------------- /src/LiberatorObject.php: -------------------------------------------------------------------------------- 1 | popsCall($method, $arguments); 35 | } 36 | 37 | /** 38 | * Set the wrapped object. 39 | * 40 | * @param string $object The object to wrap. 41 | * 42 | * @throws InvalidTypeException If the supplied value is not the correct type. 43 | */ 44 | public function setPopsValue($object) 45 | { 46 | parent::setPopsValue($object); 47 | 48 | $this->liberatorReflector = new ReflectionObject($object); 49 | } 50 | 51 | /** 52 | * Call a method on the wrapped object with support for by-reference 53 | * arguments. 54 | * 55 | * @param string $method The name of the method to call. 56 | * @param array &$arguments The arguments. 57 | * 58 | * @return mixed The result of the method call. 59 | */ 60 | public function popsCall($method, array &$arguments) 61 | { 62 | if ($this->liberatorReflector->hasMethod($method)) { 63 | $method = $this->liberatorReflector->getMethod($method); 64 | $method->setAccessible(true); 65 | 66 | return $this->popsProxySubValue( 67 | $method->invokeArgs($this->popsValue(), $arguments) 68 | ); 69 | } 70 | 71 | return parent::popsCall($method, $arguments); 72 | } 73 | 74 | /** 75 | * Set the value of a property on the wrapped object. 76 | * 77 | * @param string $property The property name. 78 | * @param mixed $value The new value. 79 | */ 80 | public function __set($property, $value) 81 | { 82 | if ($propertyReflector = $this->liberatorPropertyReflector($property)) { 83 | $propertyReflector->setValue($this->popsValue(), $value); 84 | 85 | return; 86 | } 87 | 88 | parent::__set($property, $value); 89 | } 90 | 91 | /** 92 | * Get the value of a property from the wrapped object. 93 | * 94 | * @param string $property The property name. 95 | * 96 | * @return mixed The property value. 97 | */ 98 | public function __get($property) 99 | { 100 | if ($propertyReflector = $this->liberatorPropertyReflector($property)) { 101 | return $this->popsProxySubValue( 102 | $propertyReflector->getValue($this->popsValue()) 103 | ); 104 | } 105 | 106 | return parent::__get($property); 107 | } 108 | 109 | /** 110 | * Returns true if the property exists on the wrapped object. 111 | * 112 | * @param string $property The name of the property to search for. 113 | * 114 | * @return bool True if the property exists. 115 | */ 116 | public function __isset($property) 117 | { 118 | if ($propertyReflector = $this->liberatorPropertyReflector($property)) { 119 | return null !== $propertyReflector->getValue($this->popsValue()); 120 | } 121 | 122 | return parent::__isset($property); 123 | } 124 | 125 | /** 126 | * Unset a property from the wrapped object. 127 | * 128 | * @param string $property The property name. 129 | */ 130 | public function __unset($property) 131 | { 132 | if ($propertyReflector = $this->liberatorPropertyReflector($property)) { 133 | $propertyReflector->setValue($this->popsValue(), null); 134 | 135 | return; 136 | } 137 | 138 | parent::__unset($property); 139 | } 140 | 141 | /** 142 | * Get the proxy class. 143 | * 144 | * @return string The proxy class. 145 | */ 146 | protected static function popsProxyClass() 147 | { 148 | return 'Eloquent\Liberator\Liberator'; 149 | } 150 | 151 | /** 152 | * Get the class reflector. 153 | * 154 | * @return ReflectionObject The class reflector. 155 | */ 156 | protected function liberatorReflector() 157 | { 158 | return $this->liberatorReflector; 159 | } 160 | 161 | /** 162 | * Get a property reflector. 163 | * 164 | * @param string $property The property name. 165 | * 166 | * @return ReflectionProperty|null The property reflector, or null if no such property exists. 167 | */ 168 | protected function liberatorPropertyReflector($property) 169 | { 170 | $classReflector = $this->liberatorReflector(); 171 | 172 | while ($classReflector) { 173 | if ($classReflector->hasProperty($property)) { 174 | $propertyReflector = $classReflector->getProperty($property); 175 | $propertyReflector->setAccessible(true); 176 | 177 | return $propertyReflector; 178 | } 179 | 180 | $classReflector = $classReflector->getParentClass(); 181 | } 182 | 183 | return null; 184 | } 185 | 186 | private $liberatorReflector; 187 | } 188 | -------------------------------------------------------------------------------- /src/LiberatorProxyInterface.php: -------------------------------------------------------------------------------- 1 |