backend | frontend

Notes PHP (1/2)

Backend

Tehdään PHP:lla toimiva backend joka toimii notesdemon kanssa. Lisää kansio phpback notesdemon alapuolelle.

Laadi kansiorakenne

phpback
  index.php

phpback / helpers
  helpers.php

phpback / models
  connection.php
  note.php
  user.php

phpback / controlelrs
  notesManagement.php
  usersManagement.php

helpers.php

<?php

function getRoute($uri) {
    $segments = explode('/', trim($uri, '/'));
    $route = null;
    if (count($segments) > 1) {
        $route = "/" . strtolower($segments[0]) . "/" . strtolower($segments[1]); // /api/notes
    }
    else {
        $route = "/" . strtolower($segments[0]);
    }

    return $route;
}

function getId($uri) {
    $segments = explode('/', trim($uri, '/'));
    $id = $segments[2] ?? null;
    return $id;
}

function respond($statusCode, $data = null)
{
    $allowedOrigins = [
        "http://localhost:5173",
        "https://alidomain.tunnus.treok.io"
    ];

    if (isset($_SERVER['HTTP_ORIGIN']) && in_array($_SERVER['HTTP_ORIGIN'], $allowedOrigins)) {
        header("Access-Control-Allow-Origin: " . $_SERVER['HTTP_ORIGIN']);
        header("Access-Control-Allow-Credentials: true");
    }

    header("Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS");
    header("Access-Control-Allow-Headers: Content-Type, Authorization");

    // 200 = OK, 201 = Created, 204 = No content
    // 400 = Bad request, 401 = Unauthorized, 403 = Forbidden, 404 = Not found
    // 500 = Internal server error
    http_response_code($statusCode);

    // Kaikissa muissa paitsi 204-tilanteissa lähetetään JSON
    if ($statusCode !== 204) {
        header('Content-Type: application/json; charset=utf-8');
        echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    }

    exit;
}

models

connection.php

<?php
function connectDB(){
    static $connection;
    if(!isset($connection)) {
        $connection = connect();
    }      
    return $connection;
}

function connect() {
    
    $host = getenv('DB_HOST', true) ?: "localhost";
    $port = getenv('DB_PORT', true) ?: 3306; 
    $dbname = getenv('DB_NAME', true) ?: "notesdemo_db"; 
    $user = getenv('DB_USERNAME', true) ?: "root"; 
    $password = getenv('DB_PASSWORD', true) ?: "mypass123"; 
   
    $connectionString = "mysql:host=$host;dbname=$dbname;port=$port;charset=utf8";

    try {       
            $pdo = new PDO($connectionString, $user, $password);
            $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
            return $pdo;
    } catch (PDOException $e){
            echo "Virhe tietokantayhteydessä: " . $e->getMessage();
            die();
    }
}

note.php

<?php
require_once "connection.php";

function addNote(string $content, $date, $important, int $userid): int
{
    $pdo = connectDB();
    $important = $important ? 1 : 0; // mysql boolean
    $stmt = $pdo->prepare('INSERT INTO notes (content, `date`, important, user_id) VALUES (?, ?, ?, ?)');
    $stmt->execute([$content, $date, $important, $userid]);
    $last_id = $pdo->lastInsertId();
    return $last_id;
}

function getNotes(int $userid): array
{
    $pdo = connectDB();
    $sql = "SELECT * FROM notes WHERE user_id = ?";
    $stmt = $pdo->prepare($sql);
    $stmt->execute([$userid]);
    return $stmt->fetchAll(PDO::FETCH_ASSOC);
}

function deleteNote(int $id): void
{
    $pdo = connectDB();
    $stmt = $pdo->prepare('DELETE FROM notes WHERE id = ?');
    $stmt->execute([$id]);
}

function updateNote(int $id, string $content, bool $important): void
{
    $pdo = connectDB();
    $important = $important ? 1 : 0; // mysql boolean
    $stmt = $pdo->prepare('UPDATE notes SET content = ?,important = ? WHERE id = ?');
    $stmt->execute([$content, $important, $id]);
}

user.php

<?php
require_once "connection.php";

function isEmailTaken(string $email) {
    $pdo = connectDB();
    $stmt = $pdo->prepare('SELECT * FROM users WHERE email = ?');
    $stmt->execute([$email]);
    return $stmt->fetch(PDO::FETCH_ASSOC);
}

function registerUser(string $username, string $email, string $password) {
    $pdo = connectDB();

    if (isEmailTaken($email)) {
        throw new Exception('Email already exists.');
    }

    $stmt = $pdo->prepare('INSERT INTO users (username, email, password) VALUES (?, ?, ?)');
    $stmt->execute([$username, $email, password_hash($password, PASSWORD_DEFAULT)]);
}

function login(string $username, string $password) {
    $pdo = connectDB();
    $stmt = $pdo->prepare('SELECT * FROM users WHERE username = ?');
    $stmt->execute([$username]);
    $user = $stmt->fetch(PDO::FETCH_ASSOC);
    
    if (!$user) {
        return false;
    }
    else if (!password_verify($password, $user['password'])) {
        return false;
    }
    else {
        return $user;
    }
}

controllers

notesManagement.php

<?php
require_once "./models/note.php";

function updateNoteController()
{
    if (!isset($_SESSION['userid'])) {
        respond(401, ['message' => 'Not logged in']);
    }
    // viesti request bodyssa:
    $note = json_decode(file_get_contents('php://input'));
    if (isset($note->id, $note->content, $note->important) && !is_null($note->id) && !is_null($note->content) && !is_null($note->important)) {
        $id = $note->id;
        $content = $note->content;
        $important = $note->important;
        try {
            updateNote($id, $content, $important);
            respond(204); // 204 no content
        } catch (Exception $e) {
            respond(500, ['message' => $e->getMessage()]);
        }
    } else {
        respond(400, ['message' => 'Provided JSON is invalid.']);
    }
}

function deleteNoteController(int $id)
{
    if (!isset($_SESSION['userid'])) {
        respond(401, ['message' => 'Not logged in']);
    }
    if (isset($id) && !is_null($id)) {
        try {
            deleteNote($id);
            respond(204); // 204 no content
        } catch (Exception $e) {
            respond(500, ['message' => $e->getMessage()]);
        }
    } else {
        respond(400, ['message' => 'Provided JSON is invalid.']);
    }
}

function addNoteController()
{
    if (!isset($_SESSION['userid'])) {
        respond(401, ['message' => 'Not logged in']);
    }
    try {
        $note = json_decode(file_get_contents('php://input'));
        if (isset($note->content,$note->important) && !is_null($note->content) && !is_null($note->important)) {
            $content = $note->content;
            $important = $note->important;
            $date = date("Y-m-d");
            $userid = $_SESSION['userid'];

            try {
                $id = addNote($content,$date, $important, $userid);
                $data = array('content' => $content,'date' => $date, 'important' => $important, 'id' => $id);
                respond(201, $data); // 201 created
            } catch (Exception $e) {
                respond(500, ['message' => $e->getMessage()]);
            }
        }
    } catch (Exception $e) {
        respond(500, ['message' => $e->getMessage()]);
    }
}

function getNotesController()
{
    if (!isset($_SESSION['userid'])) {
        respond(401, ['message' => 'Not logged in']);
    }
    try {
        $userid = $_SESSION['userid'];
        $notes = getNotes($userid);
        if (is_null($notes)) {
            respond(500, ['message' => 'Internal server error, please contact the administrator.']);
        } else {
            respond(200, $notes);
        }
    } catch (Exception $e) {
        respond(500, ['message' => $e->getMessage()]);
    }
}

usersManagement.php

<?php
require_once "./models/user.php";

function registerController() {
    $user = json_decode(file_get_contents('php://input'));
    if (isset($user->username, $user->email, $user->password) && !is_null($user->username) && !is_null($user->email) && !is_null($user->password)) {
        $username = $user->username;
        $password = $user->password;
        $email = $user->email;
        try {
            registerUser($username, $email, $password);
            respond(201, ['user' => $user]);
        } catch (Exception $e) {
            respond(500, ['message' => $e->getMessage()]);
        }
    } else {
        respond(400, ['message' => 'Provided JSON is invalid.']);
    }
}

function loginController() {
    $user = json_decode(file_get_contents('php://input'));
    //echo json_encode(['useri' => $user]);
    if (isset($user->username, $user->password) && !is_null($user->username) && !is_null($user->password)) {
        $username = $user->username;
        $password = $user->password;
        try {
            $user = login($username, $password);
            if ($user === false) {
                respond(401, ['message' => 'Invalid credentials.']);
                return;
            }
            $_SESSION['username'] = $user['username'];
            $_SESSION['userid'] = $user['id'];
            $_SESSION['session_id'] = session_id();
            respond(200, ['user' => $user]);
        } catch (Exception $e) {
            respond(500, ['message' => $e->getMessage()]);
        }
    } else {
        respond(400, ['message' => 'Provided JSON is invalid.']);
    }
}

function logoutController() {
    session_unset();
    session_destroy();
    respond(200,['message' => 'Logged out']);
}

function meController() {
    if (isset($_SESSION['userid'])) {
        respond(200, [
            'id' => $_SESSION['userid'],
            'username' => $_SESSION['username']
        ]);
    } else {
        // toinen tapa olisi lähettää 401
        // siitä virheilmoitus frontin konsolissa
        respond(200, [
            'id' => null,
            'username' => null
        ]);
    }
}

index.php

Sijoita index.php juurihakemistoon. Täällä käsitellään reititys ja ohjataan pyyntö oikeaan paikkaan.

<?php
session_start();

require_once "./helpers/helpers.php";
require_once './controllers/notesManagement.php';
require_once './controllers/usersManagement.php';

// GET, POST, DELETE, OPTIONS, PUT
$method = $_SERVER['REQUEST_METHOD']; 
$uri = explode("?", $_SERVER["REQUEST_URI"])[0];

$route = getRoute($uri); // /api/notes
$id = getId($uri); // /api/notes/1

if ($method === 'OPTIONS') {
    respond(200);
    exit();
}

switch ($route) {
    case "/api/me":
        meController();
        break;
        
    case "/api/register":
        registerController();
        break;

    case "/api/login":
        loginController();
        break;

    case "/api/logout":
        logoutController();
        break;

    case "/api/notes":
        switch ($method) {
            case 'GET':
                getNotesController();
                break;

            case 'POST':
                addNoteController();
                break;

            case 'DELETE':
                if ($id) deleteNoteController($id);
                break;

            case 'PUT':
                if ($id) updateNoteController($id);
                break;
        }
        break;

    default:
        respond(404);
}