01 Doing the files and folders - HugoEsparzaC/CiCompOverflow GitHub Wiki

Structuring project folders

Make the following folders:

  • controllers
  • includes
  • models
  • public
  • src
    • img
    • js
    • scss
      • base
  • views

Do the files

controllers

.gitkeep

void

includes

.env

DB_HOST = 
DB_USER = 
DB_PASS = 
DB_NAME = 

app.php

<?php

use Model\ActiveRecord;
require __DIR__ . '/../vendor/autoload.php';
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
$dotenv->safeLoad();

require 'functions.php';
require 'database.php';

// Connect to the database
ActiveRecord::setDB($db);

database.php

<?php

$db = mysqli_connect(
    $_ENV['DB_HOST'],
    $_ENV['DB_USER'],
    $_ENV['DB_PASS'],
    $_ENV['DB_NAME'],
);

$db->set_charset('utf8');

if (!$db) {
    echo "Error: Failed to connect to MySQL.";
    echo "Debugging errno: " . mysqli_connect_errno();
    echo "Debugging error: " . mysqli_connect_error();
    exit;
}

functions.php

<?php

function debuguear($variable) : string {
    echo "<pre>";
    var_dump($variable);
    echo "</pre>";
    exit;
}

// Escape / Sanitize HTML
function s($html) : string {
    $s = htmlspecialchars($html);
    return $s;
}

models

ActiveRecord.php

<?php
namespace Model;
class ActiveRecord {
    public $id;

    // Database
    protected static $db;
    protected static $table = '';
    protected static $dbColumns = [];

    // Alerts and Messages
    protected static $alerts = [];
    
    // Define the connection to the DB - includes/database.php
    public static function setDB($database) {
        self::$db = $database;
    }

    public static function setAlert($type, $message) {
        static::$alerts[$type][] = $message;
    }

    // Validation
    public static function getAlerts() {
        return static::$alerts;
    }

    public function validate() {
        static::$alerts = [];
        return static::$alerts;
    }

    // Execute the SQL query to get the results
    public static function querySQL($query) {
        // Execute the query
        $result = self::$db->query($query);

        // Iterate the results
        $array = [];
        while($record = $result->fetch_assoc()) {
            $array[] = static::createObject($record);
        }

        // Free the memory
        $result->free();

        // Return the results
        return $array;
    }

    // Create the object in memory that is equal to the DB
    protected static function createObject($record) {
        $object = new static;

        foreach($record as $key => $value ) {
            if(property_exists( $object, $key  )) {
                $object->$key = $value;
            }
        }

        return $object;
    }

    // Identify and join the attributes of the DB
    public function attributes() {
        $attributes = [];
        foreach(static::$dbColumns as $column) {
            if($column === 'id') continue;
            $attributes[$column] = $this->$column;
        }
        return $attributes;
    }

    // Sanitize the attributes before saving them to the DB
    public function sanitizeAttributes() {
        $attributes = $this->attributes();
        $sanitized = [];
        foreach($attributes as $key => $value) {
            $sanitized[$key] = self::$db->escape_string($value);
        }
        return $sanitized;
    }

    // Synchronize the object in memory with the changes made by the user
    public function synchronize($args = []) {
        foreach($args as $key => $value) {
            if(property_exists($this, $key) && !is_null($value)) {
                $this->$key = $value;
            }
        }
    }

    // Save a record
    public function save() {
        $result = '';
        if (!is_null($this->id)) {
            // Update
            $result = $this->update();
        } else {
            // Creating a new record
            $result = $this->create();
        }
        return $result;
    }

    // Get all records
    public static function all() {
        $query = "SELECT * FROM " . static::$table;
        $result = self::querySQL($query);
        return $result;
    }

    // Search for a record by its id
    public static function find($id) {
        $query = "SELECT * FROM " . static::$table  ." WHERE id = {$id}";
        $result = self::querySQL($query);
        return array_shift( $result ) ;
    }

    // Get a limited number of records
    public static function get($limit) {
        $query = "SELECT * FROM " . static::$table . " LIMIT {$limit}";
        $result = self::querySQL($query);
        return array_shift($result);
    }

    // Create a new record
    public function create() {
        // Sanitize the data
        $attributes = $this->sanitizeAttributes();
        // Insert into the database
        $query = "INSERT INTO " . static::$table . " (";
        $query .= join(', ', array_keys($attributes));
        $query .= ") VALUES ('";
        $query .= join("', '", array_values($attributes));
        $query .= "')";
        // Result of the query
        $result = self::$db->query($query);
        return [
            'result' => $result,
            'id' => self::$db->insert_id
        ];
    }

    // Update the record
    public function update() {
        // Sanitize the data
        $attributes = $this->sanitizeAttributes();
        // Iterate to add each field to the DB
        $values = [];
        foreach ($attributes as $key => $value) {
            $values[] = "{$key}='{$value}'";
        }
        // SQL query
        $query = "UPDATE " . static::$table . " SET ";
        $query .= join(', ', $values);
        $query .= " WHERE id = '" . self::$db->escape_string($this->id) . "' ";
        $query .= " LIMIT 1";
        // Update DB
        $result = self::$db->query($query);
        return $result;
    }

    // Delete a record by its ID
    public function delete() {
        $query = "DELETE FROM " . static::$table . " WHERE id = " . self::$db->escape_string($this->id) . " LIMIT 1";
        $result = self::$db->query($query);
        return $result;
    }

}

public

index.php

<?php 

require_once __DIR__ . '/../includes/app.php';

use MVC\Router;

$router = new Router();

// Checks and validates the routes that exist and assigns them the Controller functions
$router->checkRoutes();

img

Images

js

app.js

void

scss

app.scss

@forward 'base';

base

_globals.scss

@use 'variables' as v;

html {
    font-size: 62.5%;
    box-sizing: border-box;
    height: 100%;
}
body {
    font-family: v.$main_font;
    font-size: 1.6rem;
    font-style: normal;
}
*, *:before, *:after {
    box-sizing: inherit;
}
.contenedor {
    width: 95%;
    max-width: 120rem;
    margin: 0 auto;
}
a {
    text-decoration: none;
}
h1, h2, h3 {
    margin: 0 0 5rem 0;
    font-weight: 900;
}
h1 {
    font-size: 4rem;
}
h2 {
    font-size: 4.6rem;
}
h3 {
    font-size: 6rem;
    text-align: center;
}
img {
    max-width: 100%;
    width: 100%;
    height: auto;
    display: block;
}

_index.scss

@forward 'globals';
@forward 'mixins';
@forward 'normalize';
@forward 'variables';

_mixins.scss

@use 'variables' as v;

/** Media Queries **/
@mixin phone {
    @media (min-width: v.$phone) {
        @content;
    }
}
@mixin tablet {
    @media (min-width: v.$tablet) {
        @content;
    }
}
@mixin desktop {
    @media (min-width: v.$desktop) {
        @content;
    }
}

@mixin grid($columns: 1, $spacing: 5rem) {
    display: grid;
    grid-template-columns: repeat($columns, 1fr);
    gap: $spacing;
}

_normalize

normalize.css

_variables

// Font
$main_font: "Montserrat", sans-serif;

// Media Query Sizes
$phone: 480px;
$tablet: 768px;
$desktop: 1024px;

// Computer Science ColorMagic Colors
$darknavyblue: #191d4d;
$navyblue: #505086;
$indigo:#8787b5;
$grey: #d6d6e0;
$clearindigo: #b0b0c9;

// Basic Colors
$black: #000000;
$white: #FFFFFF;

$spacing: 5rem;

// Weights
$thin: 300;
$regular: 400;
$bold: 700;
$black: 900;

views

layout.php

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>CiComp Overflow</title>
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,100..900;1,100..900&display=swap" rel="stylesheet">
    <link rel="preload" href="build/css/app.css" as="style">
    <link rel="stylesheet" href="build/css/app.css">
</head>
<body>

    <?php echo $content; ?>
            
</body>
</html>

root folder

.gitignore

# Dependencias de Composer
vendor/

# Dependencias de Node.js
node_modules/

# Variables de entorno
.env

# Archivos de logs y temporales
*.log
*.tmp

gulpfile.js

import path from 'path'
import fs from 'fs'
import { glob } from 'glob'
import { src, dest, watch, series } from 'gulp'
import * as dartSass from 'sass'
import gulpSass from 'gulp-sass'
import terser from 'gulp-terser'
import sharp from 'sharp'

const sass = gulpSass(dartSass);

const paths = {
    scss: 'src/scss/**/*.scss',
    js: 'src/js/**/*.js',
    imagenes: 'src/img/**/*'
}

export function css( done ) {
    src(paths.scss, {sourcemaps: true})
        .pipe( sass({
            outputStyle: 'compressed'
        }).on('error', sass.logError) )
        .pipe( dest('./public/build/css', {sourcemaps: '.'}) );
    done()
}

export function js( done ) {
    src(paths.js, {sourcemaps: true})
      .pipe(terser())
      .pipe(dest('./public/build/js', {sourcemaps: '.'}))
    done()
}

/* 
export async function crop(done) {
    const inputFolder = 'src/img/gallery/full'
    const outputFolder = 'src/img/gallery/thumb';
    const width = 250;
    const height = 180;
    if (!fs.existsSync(outputFolder)) {
        fs.mkdirSync(outputFolder, { recursive: true })
    }
    const images = fs.readdirSync(inputFolder).filter(file => {
        return /\.(jpg)$/i.test(path.extname(file));
    });
    try {
        images.forEach(file => {
            const inputFile = path.join(inputFolder, file)
            const outputFile = path.join(outputFolder, file)
            sharp(inputFile) 
                .resize(width, height, {
                    position: 'centre'
                })
                .toFile(outputFile)
        });

        done()
    } catch (error) {
        console.log(error)
    }
}
*/

export async function imagenes(done) {
    const srcDir = './src/img';
    const buildDir = './public/build/img';
    const images =  await glob('./src/img/**/*{jpg,png}')

    images.forEach(file => {
        const relativePath = path.relative(srcDir, path.dirname(file));
        const outputSubDir = path.join(buildDir, relativePath);
        procesarImagenes(file, outputSubDir);
    });
    done();
}

function procesarImagenes(file, outputSubDir) {
    if (!fs.existsSync(outputSubDir)) {
        fs.mkdirSync(outputSubDir, { recursive: true })
    }
    const baseName = path.basename(file, path.extname(file))
    const extName = path.extname(file)

    if (extName.toLowerCase() === '.svg') {
        // If it's an SVG file, move it to the output directory
        const outputFile = path.join(outputSubDir, `${baseName}${extName}`);
        fs.copyFileSync(file, outputFile);
    } else {
        // For other image formats, process them with sharp
        const outputFile = path.join(outputSubDir, `${baseName}${extName}`);
        const outputFileWebp = path.join(outputSubDir, `${baseName}.webp`);
        const outputFileAvif = path.join(outputSubDir, `${baseName}.avif`);
        const options = { quality: 80 };

        sharp(file).jpeg(options).toFile(outputFile);
        sharp(file).webp(options).toFile(outputFileWebp);
        sharp(file).avif().toFile(outputFileAvif);
    }
}

export function dev( done ) {
    watch( paths.scss, css );
    watch( paths.js, js );
    watch( paths.imagenes + '{png,jpg}', imagenes)
    done()
}

export default series( js, css, imagenes, dev )

Router.php

<?php
namespace MVC;

class Router
{
    public array $getRoutes = [];
    public array $postRoutes = [];

    public function get($url, $fn)
    {
        $this->getRoutes[$url] = $fn;
    }

    public function post($url, $fn)
    {
        $this->postRoutes[$url] = $fn;
    }

    public function checkRoutes()
    {
        // Protect Routes...
        session_start();

        // Array of protected routes...
        // $protected_routes = ['/admin', '/properties/create', '/properties/update', '/properties/delete', '/sellers/create', '/sellers/update', '/sellers/delete'];
        // $auth = $_SESSION['login'] ?? null;

        $currentUrl = $_SERVER['PATH_INFO'] ?? '/';
        $method = $_SERVER['REQUEST_METHOD'];

        if ($method === 'GET') {
            $fn = $this->getRoutes[$currentUrl] ?? null;
        } else {
            $fn = $this->postRoutes[$currentUrl] ?? null;
        }

        if ( $fn ) {
            // Call user fn will call a function when we don't know which one it will be
            call_user_func($fn, $this); // This is to pass arguments
        } else {
            echo "Page Not Found or Invalid Route";
        }
    }

    public function render($view, $datos = [])
    {
        // Read what we pass to the view
        foreach ($datos as $key => $value) {
            $$key = $value;  // Double dollar sign means: variable variable, basically our variable remains the original, but when assigning it to another it does not overwrite it, it keeps its value, this way the variable name is assigned dynamically
        }

        ob_start(); // Store in memory for a moment...

        // then include the view in the layout
        include_once __DIR__ . "/views/$view.php";
        $content = ob_get_clean(); // Clean the Buffer
        include_once __DIR__ . '/views/layout.php';
    }
}
⚠️ **GitHub.com Fallback** ⚠️