rizal / management-login
Management Login MVC
Installs: 7
Dependents: 0
Suggesters: 0
Security: 0
Stars: 3
Watchers: 1
Forks: 0
Open Issues: 0
Type:project
pkg:composer/rizal/management-login
Requires
- php: >=8
Requires (Dev)
- phpunit/phpunit: 9.5.8
README
About The results of learning the basics of PHP programming language, object-oriented programming (OOP), unit testing, web PHP, Composer, MVC, logging, and software development architecture techniques have been documented in this case study for learning purposes only
How to install the project
Create project via composer
composer create-project rizal/management-login
If your not have composer, u can create project via github clone
git clone https://github.com/RizalFIrdaus/PHP-Login-Management.git
If your not have git on your local computer, you can download manually in github (Download ZIP)
Setup
Dependency Composer
After installing the project, you may have this project without vendor dependency, insert dependency like down below
composer install composer dump-autoload
Dump-autoload is optional if your changes any autoload in composer.json
Database
This project is required mysql databases, to config database environment, you can open env folder then open database.php To configure the database in the getDataConfig function, if it is only used for production, the database used is prod. However, if you want to perform testing implementation, the configuration must also include the test database
"database" => [ "prod" => [ "url" => "mysql:host=localhost:3306;dbname=php_login_management", "username" => "root", "password" => "" ], "test" => [ "url" => "mysql:host=localhost:3306;dbname=php_login_management_test", "username" => "root", "password" => "" ] ]
Query Create Database & Tables Prod&Test
After configuration, you will need to create the database and tables that are appropriate for the scope of this project. Here is a query for executing SQL database for testing and production
CREATE DATABASE php_login_management; CREATE DATABASE php_login_management_test; CREATE TABLE users( id VARCHAR(255) PRIMARY KEY, name VARCHAR(255) NOT NULL, password VARCHAR(255) NOT NULL ) CREATE TABLE sessions( id VARCHAR(255) PRIMARY KEY, user_id VARCHAR(255) NOT NULL, ) ALTER TABLE sessions ADD CONSTRAINT fk_session_user FOREIGN KEY (user_id) REFERENCES users(id);
Running Project
To run this project :
php -S localhost:8080 -t public
The Project can run with port 8080 if your set port like step before
Roadmap
- PHP-Login-Management
Structure Database
We have 2 databases : (1) php_login_management and (2) php_login_management_test
Database (1) is original based for raw data, while database (2) is replica from database (1) that contains dirty raw data for used testing
CREATE DATABASE php_login_management; CREATE DATABASE php_login_management_test; CREATE TABLE users( id VARCHAR(255) PRIMARY KEY, name VARCHAR(255) NOT NULL, password VARCHAR(255) NOT NULL ) CREATE TABLE sessions( id VARCHAR(255) PRIMARY KEY, user_id VARCHAR(255) NOT NULL, ) ALTER TABLE session ADD CONSTRAINT fk_session_user FOREIGN KEY (user_id) REFERENCES users(id);
Architecture Software
MVC - Repository Pattern
Database GetConection
The creation of getConnection uses a singleton architecture, which means creating an object once and using it multiple times. The getConnection function will accept a "prod" or "test" parameter. The default is set to "test"
Env
function getDataConfig(): array { return [ "database" => [ "prod" => [ "url" => "mysql:host=localhost:3306;dbname=php_login_management", "username" => "root", "password" => "" ], "test" => [ "url" => "mysql:host=localhost:3306;dbname=php_login_management_test", "username" => "root", "password" => "" ] ] ]; }
getConnection Function
public static function getConnection(string $env = "test"): \PDO { // Singleton architecture (User single to many action) // if pdo null then create new db if not .. just return back pdo had been created if (self::$pdo == null) { // create new database require_once __DIR__ . "/../../env/database.php"; $config = getDataConfig(); self::$pdo = new PDO( $config["database"][$env]["url"], $config["database"][$env]["username"], $cofing["database"][$env]["password"] ); } return self::$pdo; }
Testing Connection
During testing, two data connection tests will be performed to ensure that the data is not null and to verify that the implemented singleton architecture is functioning properly.
public function testConnection() { $connection = Database::getConnection(); self::assertNotNull($connection); } public function testSingletonDatabase() { $con1 = Database::getConnection(); $con2 = Database::getConnection(); self::assertSame($con1, $con2); }
The Result
PHPUnit 9.5.8 by Sebastian Bergmann and contributors. .. 2 / 2 (100%) Time: 00:00.054, Memory: 4.00 MB OK (2 tests, 2 assertions)
View Templating
Templating is used to separate the header and footer for clean code in the visual aspect. By separating the header, footer, and body, we only need to import the header and footer while the body is always changing
public static function render(string $view, $model) { require __DIR__ . "/../View/Layouts/header.php"; require __DIR__ . '/../View/' . $view . '.php'; require __DIR__ . "/../View/Layouts/footer.php"; }
Testing View
This testing will check the expected regex displayed on the website page.
public function testRender() { View::render("Home/index", [ "title" => "Login" ]); self::expectOutputRegex("[Login]"); self::expectOutputRegex("[Register]"); }
The Result
PHPUnit 9.5.8 by Sebastian Bergmann and contributors.
. 1 / 1 (100%)
Time: 00:00.067, Memory: 4.00 MB
OK (1 test, 1 assertion)
Repository
This architecture uses the MVC architecture and is combined with the Repository Pattern to avoid overwhelming logic in the MVC Controller. The Controller will call the Service, the Service will call the Repository, and the Repository will retrieve data from the Domain and directly access the database
Domain
The Domain represents the table data in the database.
class User { private string $id; private string $name; private string $password; // SETTER AND GETTER public function getId(): string { return $this->id; } public function getName(): string { return $this->name; } public function getPassword(): string { return $this->password; } public function setId(string $id): void { $this->id = $id; } public function setName(string $name): void { $this->name = $name; } public function setPassword(string $password): void { $this->password = $password; } }
User Repository
The User Repository has several methods such as save for saving data into the database, getById for retrieving data based on Id, and deleteAll for testing purposes
class UserRepository { private \PDO $connection; public function __construct(\PDO $connection) { $this->connection = $connection; } // Save user into database public function save(User $user): User { $statement = $this->connection->prepare("INSERT INTO users(id,name,password) VALUES (?,?,?)"); $statement->execute([ $user->getId(), $user->getName(), $user->getPassword() ]); return $user; } public function getById(string $id): ?User { $statement = $this->connection->prepare("SELECT id,name,password FROM users WHERE id=?"); $statement->execute([$id]); // Trying to fetch data from id try { if ($row = $statement->fetch()) { $user = new User(); $user->setId($row["id"]); $user->setName($row["name"]); $user->setPassword($row["password"]); return $user; } else { return null; } } finally { // Close Query $statement->closeCursor(); } } public function deleteAll(): void { $this->connection->exec("DELETE FROM users"); } }
Testing User Repository
Testing the User Repository will check if method user has saved data into the database and the second one will check if method getById is null
private UserRepository $userRepository; public function setUp(): void { $this->userRepository = new UserRepository(Database::getConnection()); $this->userRepository->deleteAll(); } public function testSaveSuccess() { $user = new User(); $user->setId("rizal"); $user->setName("rizal"); $user->setPassword("rahasia"); $this->userRepository->save($user); $result = $this->userRepository->getById($user->getId()); self::assertNotNull($result); self::assertEquals($user->getId(), $result->getId()); } public function testIdNotFound() { $user = $this->userRepository->getById("awdawd"); self::assertNull($user); }
The Result
PHPUnit 9.5.8 by Sebastian Bergmann and contributors. .. 2 / 2 (100%) Time: 00:00.033, Memory: 4.00 MB OK (2 tests, 3 assertions)
Service
The Service is the centralized business logic. The Service will call the Repository to use available methods. The Repository calls the Domain and is directly connected to the database.
Registration Service
The registration method will validate incoming requests, which will be handled by the validationUserRegistrationRequest. If it passes the registration stage, it will then check if the request ID has already been created. If it has, it will give an exception, otherwise, a new user will be created. This method will provide a response in the form of user data by creating a UserRegistrationResponse model, while the UserRegistrationRequest will handle the incoming request in the user registration request
public function register(UserRegistrationRequest $request): UserRegistrationResponse { // Validation $this->validationUserRegistrationRequest($request); try { Database::beginTransaction(); $user = $this->userRepository->getById($request->id); // Checking if user already exist if ($user != null) { throw new ValidationException("Id $request->id already exist !"); } // Create new user if id ready to save $user = new User(); $user->setId($request->id); $user->setName($request->name); $user->setPassword(password_hash($request->password, PASSWORD_BCRYPT)); $this->userRepository->save($user); // Return response $response = new UserRegistrationResponse(); $response->user = $user; return $response; Database::commitTransaction(); } catch (\Exception $exception) { Database::rollbackTransaction(); throw $exception; } }
validationUserRegistrationRequest
public function validationUserRegistrationRequest(UserRegistrationRequest $request) { if ( $request->id == null || $request->name == null || $request->password == null || trim($request->id) == "" || trim($request->name) == "" || trim($request->password) == "" ) { throw new ValidationException("Id,name or password can't blank", 403); } else if (strlen($request->id) <= 6 || strlen($request->password) <= 8) { throw new ValidationException("Id can't less then 6 or Password can't less then 8", 403); } }
Testing User Service
This testing runs three methods: success, failed, and duplicate ID
public function testServiceSuccess() { $request = new UserRegistrationRequest(); $request->id = "rizal300500"; $request->name = "Rizal"; $request->password = "rahasia123"; $response = $this->userService->register($request); // Checking response not null self::assertNotNull($response); // Checking request id equals response id self::assertEquals($request->id, $response->user->getId()); // Verify hashing password self::assertTrue(password_verify($request->password, $response->user->getPassword())); } public function testServiceFailed() { $this->expectException(ValidationException::class); $request = new UserRegistrationRequest(); $request->id = ""; $request->name = ""; $request->password = ""; $this->userService->register($request); } public function testServiceDuplicate() { $user = new User(); $user->setId("esan300500"); $user->setName("Rizal"); $user->setPassword("rahasia123"); $this->repository->save($user); $this->expectException(ValidationException::class); $request = new UserRegistrationRequest(); $request->id = "esan300500"; $request->name = "Rizal"; $request->password = "rahasia123"; $this->userService->register($request); }
The Result
PHPUnit 9.5.8 by Sebastian Bergmann and contributors. ... 3 / 3 (100%) Time: 00:00.191, Memory: 4.00 MB OK (3 tests, 5 assertions)
User Controller
The Controller is handled by the UserController that implements some methods such as the index method, which centralizes the index route and passes it the get method, and the store method, which centralizes the store route and passes it the post method to store data from the client.
public function store() { // Request get from method post $request = new UserRegistrationRequest(); $request->id = $_POST["id"]; $request->name = $_POST["name"]; $request->password = $_POST["password"]; // trying to call register service if it success then redirecting to user/login // else render view register with error message from exception get message try { $this->userService->register($request); // Redirect::to("/users/login"); } catch (ValidationException $exception) { View::render("User/register", [ "title" => "Register User", "error" => $exception->getMessage() ]); } }
Testing User Controller
This testing runs five scenario: view index ,store success,store null, store failed, and store duplicate ID
public function testRegister() { $this->userController->index(); $this->expectOutputRegex("[Register]"); $this->expectOutputRegex("[Muhammad Rizal Firdaus]"); $this->expectOutputRegex("[Name]"); $this->expectOutputRegex("[Id]"); $this->expectOutputRegex("[Password]"); } public function testStoreRegisterSuccess() { $_POST["id"] = "rizal300500"; $_POST["name"] = "rizal"; $_POST["password"] = "rahasia12345"; $this->userController->store(); $this->expectOutputRegex("[]"); } public function testStoreRegisterFailedBlank() { $_POST["id"] = ""; $_POST["name"] = ""; $_POST["password"] = ""; $this->userController->store(); $this->expectOutputRegex("[Id,name or password can't blank]"); } public function testStoreRegisterFailedNull() { $this->userController->store(); $this->expectOutputRegex("[Id,name or password can't blank]"); } public function testStoreRegisterDuplicate() { $user = new User(); $user->setId("rizal300500"); $user->setName("Rizal"); $user->setPassword("rahasia12345"); $this->repository->save($user); $_POST["id"] = "rizal300500"; $_POST["name"] = "Rizal"; $_POST["password"] = "rahasia12345"; $this->userController->store(); $this->expectOutputRegex("[Id rizal300500 already exist !]"); }
The Result
PHPUnit 9.5.8 by Sebastian Bergmann and contributors. ..... 5 / 5 (100%) Time: 00:00.116, Memory: 4.00 MB OK (5 tests, 5 assertions)
Built By
Muhammad Rizal Firdaus