Make/Accessible
Lightweight PHP package that let you test singleton classes and inaccessible (private or protected) instance members., (*1)
Table of contents
Installation
composer require make/accessible
Features
- Call inaccessible methods
- Set values into inaccessible properties
- Get values from inaccessible properties
- Instantiate singleton classes
Usage
Suppose you have the class bellow:, (*2)
<?php
use Person;
/**
* Class PeopleGreeter.
*
* Greats a list of persons, nothing more.
*/
class PeopleGreeter
{
/** @var Person[] */
protected $peopleToGreet = [];
/**
* Adds a person to `$peopleToGreat`.
*/
public function addPersonToGreet(Person $person)
{
$this->peopleToGreet[] = $person;
}
/**
* Greats a `$person`.
*
* @param Person $person Person to great.
*/
protected function greet(Person $person)
{
if ($person->isWoman()) {
return 'Hi Mrs. ' . $person->name();
} else {
return 'Hello Mr. ' . $person->name();
}
}
/**
* Greats everyone on `$peopleToGreat`.
*/
public function greetEveryone()
{
foreach ($this->peopleToGreet as $person) {
echo $this->greet($person);
}
}
}
This class is correct, it does'nt exposes its inner elements (encapsulation) and was designed to do what is supposed to do., (*3)
But, how are you going to test this class? Test if the addPersonToGreet()
method is really adding $person
into $peopleToGreet
and greet()
method is really distinguishing mens from womens?, (*4)
You can just make all your class members public, but that way you're breaking encapsulation, thats not good., (*5)
So, what can be done here?, (*6)
Solutions
One way to test addPersonToGreet()
would be by directly accessing $peopleToGreet
and check if $person
is really there:, (*7)
$person = new Person('John Doe');
$peopleGreeter = new PeopleGreeter();
$peopleGreeter->addPersonToGreet($person);
$this->assertTrue(in_array($person, $peopleGreeter->peopleToGreet));
One way to test greet()
would be by directly accessing it:, (*8)
$person = new Person('John Doe');
$this->assertEquals("Hello Mr. John Doe", (new PeopleGreeter())->greet($person));
But of course these methods will fail, its will result in:, (*9)
Error: Cannot access protected property PeopleGreeter::$peopleToGreet.
Error: Cannot access protected method PeopleGreeter::greet().
because we can't access protected and private members outside of they parent scope., (*10)
Another way that works only with protected members is by extending their parent to another class that exposes its methods:, (*11)
class ExtendedPeopleGreeter extends PeopleGreeter
{
public function greet($person)
{
return parent::greet($person);
}
public function getPeopleToGreet()
{
return parent::$peopleToGreet;
}
}
$person = new Person('John Doe');
$this->assertEquals("Hello Mr. John Doe", (new ExtendedPeopleGreeter())->greet($person));
$person = new Person('John Doe');
$peopleGreeter = new ExtendedPeopleGreeter();
$peopleGreeter->addPersonToGreet($person);
$this->assertTrue(in_array($person, $peopleGreeter->getPeopleToGreet()));
Pros:, (*12)
- You can test protected members.
Cons:, (*13)
- Don't works with private members.
- Makes really hard write simples tests.
- You need to write a fake class every time you want to test a real class.
- You need to write a method for each inaccessilble member you want to test.
But of course there's another solution: Using PHP Reflection:, (*14)
$person = new Person('John Doe');
$peopleGreeter = new PeopleGreeter();
$peopleGreeter->addPersonToGreet($person);
$reflectedClass = new \ReflectionClass($peopleGreeter);
$propertyName = 'peopleToGreet';
if ($reflectedClass->hasProperty($propertyName)) {
$property = $reflectedClass->getProperty($propertyName);
$property->setAccessible(true);
$this->assertTrue(in_array($person, $property->getValue($peopleGreeter)));
}
$person = new Person('John Doe');
$reflectedClass = new \ReflectionClass($peopleGreeter);
$propertyName = 'peopleToGreet';
$peopleGreeter = new PeopleGreeter();
$reflectedClass = new \ReflectionClass($peopleGreeter);
$methodName = 'greet';
if ($reflectedClass->hasMethod($methodName)) {
$method = $reflectedClass->getMethod($methodName);
$method->setAccessible(true);
$returnedValue = $method->invokeArgs($peopleGreeter, ['person' => $person]);
$this->assertEquals("Hello Mr. John Doe", $returnedValue);
}
Pros:, (*15)
- Works for both protected and private members.
Cons:, (*16)
- You need to write a lot of extra code to make simple tests.
- You need to write code that also need to be tested.
- Its hard to reuse this code in other test either in other projects.
We're testing only 2 methods, now think about 30 or 50??? No, no... Forget about it!, (*17)
Using Make/Accessible
$person = new Person('John Doe');
$peopleGreeter = new PeopleGreeter();
$peopleGreeter->addPersonToGreet($person);
$accessiblePeopleGreeter = Make::accessible($peopleGreeter);
$this->assertTrue(in_array($person, $accessiblePeopleGreeter->peopleToGreet));
$person = new Person('John Doe');
$peopleGreeter = new PeopleGreeter();
$accessiblePeopleGreeter = Make::accessible($peopleGreeter);
$this->assertEquals("Hello Mr. John Doe", $accessiblePeopleGreeter->greet($person));
Did you saw that??? Just one line of code and we made our tests!!!, (*18)
We gain access to our PeopleGreeter::class
inaccessible members in a common and friendly way! ;), (*19)
Pros:, (*20)
- User friendly.
- Just one line of code.
- Makes tests really easy.
- Works for both private and protected members.
- Don't require write fake classes or methods for each test.
- Encourages the usage of encapsulation in projects providing a more isolated and flexible environment.
Cons:, (*21)
As you can see, with Make/Accessible you can now make your projects encapsulated withour fear!, (*22)
And you can also start testing singleton classes using Make/Accessible. Feature documentation soon., (*23)
Best practices
We highly recommend the use of this package for tests purposes only., (*24)
Avoid use this package to gain access to encapsulated classes, since it's like break the door of someone's house that does'nt want you to get inside., (*25)
If you're testing the same class the same way, we recommend create a function getAccessibleInstance()
at the bottom of your test class and there you make the instantiation, mocks, everything you need to instantiate the class you need to test:, (*26)
class PeopleGreeterTest extends TestCase
{
public function testGreet()
{
$person = new Person('John Doe');
$peopleGreeter = $this->getAccessibleInstance();
$this->assertEquals("Hello Mr. John Doe", $peopleGreeter->greet($person));
}
Others test functions...
public function getAccessibleInstance()
{
$instance = new PeopleGreeter();
return Make::accessible($instance);
}
}
For more tips about best practices, please, read our best practices documentation., (*27)
Support
Need a new feature? Found a bug? Please open a new issue or send me an email and we'll fix it as soon as possible., (*28)
Contribute
Feel free to contribute forking, making changes and pull requests., (*29)
Credits
License
The MakeAccessible is licensed under the MIT license. See the license file for more information., (*30)