-
-
Save fideloper/4394248 to your computer and use it in GitHub Desktop.
An example of making a User class testable and maintainable, using dependency injection and some abstraction.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
// Often, we see a class like this: | |
class User { | |
public function getCurrentUser() | |
{ | |
$user_id = $_SESSION['user_id']; | |
$user = App::db->select('user') | |
->where('id', $user_id) | |
->limit(1) | |
->get(); | |
if ( $user->num_results() > 0 ) | |
{ | |
return $user->row(); | |
} | |
return false; | |
} | |
} | |
/* | |
1. The above isn't testable. Command-line won't have $_SESSION available | |
2. Above depends on specific database implementation | |
*/ | |
// First, let's abstract this out to something more generic | |
class User { | |
public function getUser($user_id) | |
{ | |
$user = App::db->select('user') | |
->where('id', $user_id) | |
->limit(1) | |
->get(); | |
if ( $user->num_results() > 0 ) | |
{ | |
return $user->row(); | |
} | |
return false; | |
} | |
} | |
/* | |
The above can now retreive any user | |
We've removed getting the user id from session data (a dependency) from this class | |
Let's improve this - we can still remove the amount of 'knowledge' the class needs | |
*/ | |
// Next, let's start with some basic Dependency Injection | |
class User { | |
public function __construct($db_connection) | |
{ | |
$this->_db = $db_connection; | |
} | |
public function getUser($userId) | |
{ | |
$user = $this->_db->select('user') | |
->where('id', $userId) | |
->limit(1) | |
->get(); | |
if ( $user->num_results() > 0 ) | |
{ | |
return $user->row(); | |
} | |
return false; | |
} | |
} | |
/* | |
At this point, we're basically testable. We pass in the database connection and user id, so the class | |
needs no knowledge of its dependencies. We can run this in a unit test and test to see if we get results. | |
We can mock the $db_connect class, or actually pass in a real database object | |
Other than testability, at this point we can: | |
- switch out the database connection, for instance if we're switching from MySQL to PostgreSQL. | |
- pass in any user_id, instead of only getting the "current" user. | |
But we can still do better. | |
*/ | |
// Add some further abstraction | |
interface UserDataInterface { | |
public function getUser($userId); | |
} | |
class MysqlUser implements UserDataInterface { | |
public function __construct($db_connection) | |
{ | |
$this->_db = $db_connection; | |
} | |
public function getUser($userId) | |
{ | |
$user = $this->_db->select('user') | |
->where('id', $userId) | |
->limit(1) | |
->get(); | |
if ( $user->num_results() > 0 ) | |
{ | |
return $user->row(); | |
} | |
return false; | |
} | |
} | |
class User { | |
public function __construct(UserDataInterface $userdata) | |
{ | |
$this->_db = $userdata; | |
} | |
public function getUser($userId) | |
{ | |
return $this->_db->getUser($userId); | |
} | |
} | |
// Usage | |
$mysqlUser = new MysqlUser( App:db->getConnection('mysql') ); | |
$user = new User( $mysqlUser ); | |
$userId = App::session('user_id'); | |
$currentUser = $user->getUser($userId); | |
/* | |
So, what just happened? | |
1. We created an interface which will be used to get a user from our data source of choice | |
2. We created an implementation of the interface which happens to use MySQL | |
3. We enforce the use of a subclass of UserDataInterface, guaranteeing the getUser method is available | |
Results: | |
1. We keep testability thanks to DI | |
2. Enforcing the use of an interface allows us to switch the logic of how we get the user data freely. | |
We can use MySQL, other SQL, noSQL, arrays or any data source. For code | |
maintance, this means we can change out a whole storage engine and only change which class gets passed to our User class, as long | |
as it implements UserDataInterface. | |
3. Additionaly, the datasource can be fully mocked for testing. | |
*/ | |
/* | |
That's cool for testing. However, when we code, we're left with some extra leg-work. Consider the above usage: | |
*/ | |
$mysqlUser = new MysqlUser( App::db->getConnection('mysql') ); | |
$user = new User( $mysqlUser ); | |
$userId = App::session('user_id'); | |
$currentUser = $user->getUser($userId); | |
/* | |
This is an annoying process - We need to perform all these lines of code for any user-related storage! | |
We can make this easier for ourselves using a Container | |
Example: | |
@link http://twittee.org/ | |
@link http://pimple.sensiolabs.org/ | |
*/ | |
//Somewhere in a configuration file | |
$app = new Container(); | |
$app['user'] = function() { | |
return new User( new MysqlUser( App::db->getConnection('mysql') ) ); | |
} | |
/* | |
Now, anywhere in our code, we can get the User class with our MySQL connection. | |
*/ | |
$currentUser = $app['user']->getUser( App::session('user_id') ); | |
/* | |
This goes further toward making code maintainable - We can now change from MySQL | |
to another storage type in ONE location in our code! | |
*/ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/* | |
An example unit test we can now perform | |
*/ | |
use Mockery as m; | |
class UserTest extends PHPUnit_Framework_TestCase { | |
public function tearDown() | |
{ | |
m::close(); | |
} | |
public function testUserClass() | |
{ | |
$user = new User( $this->getDbMock() ); | |
$userId = 1; | |
$testUser = $user->getUser($userId); | |
$this->assertEquals($userId, $testUser->user_id); | |
} | |
protected function getDbMock() | |
{ | |
// We're not testing the MySQL connection class | |
// We're testing that User class receives a user | |
$return = new stdClass(); | |
$return->user_id = 1; | |
$return->first_name = 'Chris'; | |
$return->last_name = 'Fidao'; | |
$mock = m::mock('MysqlUser'); | |
// Expect call to getUser method once, and return our $return object | |
$mock->shouldReceive('getUser')->times(1)->andReturn($return); | |
return $mock; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Also, read this: Your code sucks, let's fix it