rizal/management-login

There is no license information available for the latest version (v1.0.0) of this package.

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

v1.0.0 2023-02-04 14:56 UTC

This package is auto-updated.

Last update: 2025-12-28 08:46:07 UTC


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

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