I've been a PHP procedural programmer for several years but I'm trying to learn OOP and I'm getting a bit confused with some patterns and principles. I would appreciate it if you could give me some tips and advice.
interface LoginAuthenticator
{
public function authenticate(UserMapper $userMapper);
}
class UserAuthenticator implements LoginAuthenticator
{
private $user;
private $session;
public function __construct(User $user, Session $session)
{
$this->user = $user;
$this->session = $session;
}
public function authenticate(UserMapper $userMapper)
{
if (!$user = $userMapper->findByUsernameAndPassword($this->user->getUsername(), $this->user->getPassword()))
{
throw new InvalidCredentialsException('Invalid username or password!');
}
$this->logUserIn($user);
}
private function logUserIn(User $user)
{
$this->session->setValue('user', $user);
}
public function logUserOut()
{
$this->session->unsetValue('user');
$this->session->destroy();
}
}
try
{
$user = new User();
$user->setUsername($_POST['username']);
$user->setPassword($_POST['password'], new MD5());
$pdo = new PDO('mysql:host=localhost;dbname=database', 'root', '');
$userAuth = new UserAuthenticator($user, new Session());
$userAuth->authenticate(new PdoUserMapper($pdo));
header('Location: index.php');
}
catch (InvalidArgumentException $e)
{
echo $e->getMessage();
}
catch (PDOException $e)
{
echo $e->getMessage();
}
catch (InvalidCredentialsException $e)
{
echo $e->getMessage();
}
Well, here is my first concern, the SRP: I don't really know if I should inject a Mapper into my UserAuthenticator::authenticate
method or if I should create a UserFinder
class and inject it instead. I don't know if it's a Mapper responsibility to find. What do you think ?
Furthermore, I'm also confused about the $user
property: my findByUsernameAndPassword
method returns a User
object, so I have two User
s instances in the same class: one injected and another returned by the Mapper. Should I inject just $username
and $password
instead of a User
object in order to authenticate ?
I have also some wrappers classes like Session and MD5 but they are not needed to understand how my classes works.
Edit
My classes after changes and user related classes:
interface Authenticator
{
public function authenticate(UserCredentials $userCredentials);
}
class LoginAuthenticator implements Authenticator
{
private $userMapper;
public function __construct(UserMapper $userMapper)
{
$this->userMapper = $userMapper;
}
public function authenticate(UserCredentials $userCredentials)
{
if (!$user = $this->userMapper->findByUsernameAndPassword($userCredentials->getUsername(), $userCredentials->getPassword()))
{
throw new InvalidCredentialsException('Invalid username or password!');
}
return $user;
}
}
class UserCredentials
{
private $username;
private $password;
public function getUsername()
{
return $this->username;
}
public function setUsername($username)
{
if (!is_string($username) || strlen($username) < 3)
{
throw new InvalidArgumentException('Invalid username.');
}
$this->username = $username;
}
public function getPassword()
{
return $this->password;
}
public function setPassword($password, Encryptor $encryptor)
{
if (!is_string($password) || strlen($password) < 8)
{
throw new InvalidArgumentException('Invalid password.');
}
$this->password = $encryptor->encrypt($password);
}
}
class User
{
private $id;
private $firstName;
private $lastName;
private $email;
private $username;
private $password;
public function getPassword()
{
return $this->password;
}
public function setPassword($password, Encryptor $encryptor)
{
$this->password = $encryptor->encrypt($password);
}
//more getters and setters
}
interface UserMapper
{
public function insert(User $user);
public function update(User $user);
public function delete($id);
public function findByUsernameAndPassword($username, $password);
public function findAll();
}
class PdoUserMapper implements UserMapper
{
private $pdo;
private $table = 'users';
public function __construct(PDO $pdo)
{
$this->pdo = $pdo;
}
public function insert(User $user)
{
$statement = $this->pdo->prepare("INSERT INTO {$this->table} VALUES(null, ?, ?, ?, ?)");
$userValues = array(
$user->getFirstName(),
$user->getLastName(),
$user->getEmail(),
$user->getUsername(),
$user->getPassword()
);
$statement->execute($userValues);
return $this->pdo->lastInsertId();
}
public function update(User $user)
{
$statement = $this->pdo->prepare("UPDATE {$this->table} SET name = ?, last_name = ?, email = ?, password = ? WHERE id = ?");
$userValues = array(
$user->getFirstName(),
$user->getLastName(),
$user->getEmail(),
$user->getPassword(),
$user->getId()
);
$statement->execute($userValues);
}
public function delete($id)
{
$statement = $this->pdo->prepare("DELETE FROM {$this->table} WHERE id = ?");
$statement->bindValue(1, $id);
$statement->execute();
}
public function findById($id)
{
$statement = $this->pdo->prepare("SELECT * FROM {$this->table} WHERE id = ?");
$statement->bindValue(1, $id);
if (!$result = $statement->execute())
{
return null;
}
$user = new User();
$user->setId($result['id']);
$user->setFirstName($result['name']);
$user->setLastName($result['last_name']);
$user->setUsername($result['username']);
$user->setEmail($result['email']);
return $user;
}
public function findByUsernameAndPassword($username, $password)
{
$statement = $this->pdo->prepare("SELECT * FROM {$this->table} WHERE username = ? AND password = ?");
$statement->bindValue(1, $username);
$statement->bindValue(2, $password);
$statement->execute();
if (!$result = $statement->fetch())
{
return null;
}
$user = new User();
$user->setId($result['id']);
$user->setFirstName($result['name']);
$user->setLastName($result['last_name']);
$user->setEmail($result['email']);
$user->setUsername($result['username']);
$user->setPassword($result['password'], new MD5());
return $user;
}
public function findAll()
{
$statement = $this->pdo->query("SELECT * FROM {$this->table}");
while ($result = $statement->fetch(PDO::FETCH_ASSOC))
{
$user = new User();
$user->setId($result['id']);
$user->setFirstName($result['name']);
$user->setLastName($result['last_name']);
$user->setUsername($result['username']);
$user->setEmail($result['email']);
$userCollection[] = $user;
}
return $userCollection;
}
}
try
{
$userCredentials = new UserCredentials();
$userCredentials->setUsername($_POST['username']);
$userCredentials->setPassword($_POST['password'], new MD5());
$pdo = new PDO('mysql:host=localhost;dbname=database', 'root', '');
$loginAuth = new LoginAuthenticator(new PdoUserMapper($pdo));
$user = $loginAuth->authenticate($userCredentials);
$session = new Session();
$session->setValue('user', $user);
header('Location: index.php');
}
catch (InvalidArgumentException $e)
{
echo $e->getMessage();
}
catch (PDOException $e)
{
echo $e->getMessage();
}
catch (InvalidCredentialsException $e)
{
echo $e->getMessage();
}
One thing that is bothering me is the need to pass an Encryptor two times: to the User::setPassword method and to UserCredentials::setPassword method. If I need to change my encryption algorithm, I'll have to change it in more than one place, what leads me to think that I'm still making something wrong.