# Framework Infrastructure and Directory System This section provides a comprehensive overview of the framework's architecture, outlining the purpose of its main directories and the overall file structure. ## Core Concepts The framework is organized into several key directories, each with a specific responsibility. This separation of concerns is crucial for maintaining a clean and scalable codebase. ### `api/` - API Endpoints _Handles all API requests in a controller-only MVC structure._ ### `app/` - Application Core & Business Logic _The heart of the application, replacing traditional models. Contains business logic, database interactions, and reusable components._ ### `public/` - User Interface _The document root for the application's UI, containing controllers, views, and static assets._ ### `commands/` - CLI Tasks _Houses command-line scripts for automated tasks, cron jobs, and internal server processing._ ### `core/` - Framework Core _Contains the foundational framework files that should rarely be modified._ ## Directory Structure ``` /www/wwwroot/(project_directory)/ ├── api/ ├── app/ ├── commands/ ├── core/ ├── plugins/ ├── public/ ├── templates/ └── vendor/ ``` ### Detailed `app/` Directory Structure ``` app/ ├── Helpers/ │ ├── DB.php # Database operations │ ├── Session.php # Session management │ ├── Validator.php # Input validation │ └── Auth.php # Authentication helpers ├── Services/ │ ├── EmailService.php │ ├── PaymentService.php │ └── NotificationService.php ├── Libraries/ │ └── CustomAuth.php └── Models/ # Optional: Domain models/entities └── Client.php ``` **Example Helper:** ```php = $min; } } ``` **Example Service:** ```php 1]); // Fetch single record $client = Db::getRow("SELECT * FROM clients WHERE id = :id", [':id' => $id]); // Insert record $clientId = Db::insert('clients', [ 'name' => 'John Doe', 'email' => 'john@example.com', 'created_at' => date('Y-m-d H:i:s') ]); // Update record $affected = Db::update('clients', ['name' => 'Jane Doe', 'updated_at' => date('Y-m-d H:i:s')], 'id = :id', [':id' => $id] ); // Delete record $affected = Db::delete('clients', 'id = :id', [':id' => $id]); // Execute custom query Db::execute("UPDATE clients SET status = :status WHERE last_login < :date", [ ':status' => 'inactive', ':date' => date('Y-m-d', strtotime('-1 year')) ]); // Transactions Db::beginTransaction(); try { Db::insert('orders', ['client_id' => $clientId, 'total' => 100]); Db::update('clients', ['balance' => 'balance - 100'], 'id = :id', [':id' => $clientId]); Db::commit(); } catch (Exception $e) { Db::rollback(); throw $e; } ``` ### Common Controller Patterns ```php // Render a view with data $this->view->data['clients'] = $clients; $this->view->render('clients/index', true, 'site'); // Render without wrapper (e.g., login page) $this->view->render('auth/login', false); // Redirect redirect('/clients'); // JSON response header('Content-Type: application/json'); echo json_encode(['success' => true, 'data' => $client]); // 404 response http_response_code(404); $this->view->render('errors/404', true, 'site'); ``` # Developer Workflows & Tutorials This section provides practical, step-by-step guides for common development tasks within the framework. ## Request Lifecycle Overview Understanding the request lifecycle is key to knowing how the framework processes a request and generates a response. 1. **Entry Point:** A user request first hits either `public/index.php` (for UI) or `api/index.php` (for API). 2. **Bootstrapping:** The `core/Bootstrap.php` file is initiated. It sets up the environment, loads the configuration from `config.php`, and initializes necessary services. 3. **Routing:** The incoming URL is processed by the routing engine. * For simple URLs, **auto-routing** maps the URL directly to a `Controller/method` pair (e.g., `/clients/show` maps to `Clients::show()`). * For complex URLs defined in the routing configuration, **AltoRouter** matches the pattern and executes the associated callback or controller. 4. **Controller Execution:** The matched controller method is called. 5. **Business Logic:** The controller interacts with the `app/` directory (e.g., calling helpers like `Db::select()`) to fetch data or perform business logic. 6. **Response Generation:** * **UI (Public):** The controller calls `$this->view->render()`, passing data to a view file. The view is rendered within a wrapper (if specified), and the final HTML is sent to the browser. * **API:** The controller typically returns data encoded as a JSON string and sets the appropriate `Content-Type` header. ## Tutorial: Creating a New Page (UI) This tutorial walks through creating a "Clients" page that displays a list of clients. ### Step 1: Create the Controller Create a new file at `public/controllers/Clients.php`. This controller will handle the logic for the clients page. ```php view->data['clients'] = $clients; $this->view->data['page_title'] = 'Our Clients'; // Render the view $this->view->render('clients/index', true, 'site'); } public function show($id) { // Fetch single client $client = Db::getRow("SELECT * FROM clients WHERE id = :id", [':id' => $id]); if (!$client) { http_response_code(404); $this->view->render('errors/404', true, 'site'); return; } // Fetch related data $orders = Db::select("SELECT * FROM orders WHERE client_id = :id ORDER BY created_at DESC", [':id' => $id] ); $this->view->data['client'] = $client; $this->view->data['orders'] = $orders; $this->view->data['page_title'] = $client['name']; $this->view->render('clients/show', true, 'site'); } public function create() { // Show create form $this->view->data['page_title'] = 'New Client'; $this->view->render('clients/create', true, 'site'); } public function store() { // Handle form submission try { // Validate input if (!Validator::required($_POST['name'])) { throw new Exception('Name is required'); } if (!Validator::email($_POST['email'])) { throw new Exception('Valid email is required'); } // Insert into database $clientId = Db::insert('clients', [ 'name' => $_POST['name'], 'email' => $_POST['email'], 'phone' => $_POST['phone'] ?? null, 'created_at' => date('Y-m-d H:i:s') ]); // Optional: Send welcome email // EmailService::sendWelcomeEmail($_POST['email'], $_POST['name']); // Redirect to the new client's page redirect('/clients/show/' . $clientId); } catch (Exception $e) { // Handle errors if (DEBUG) { echo $e->getMessage(); } else { $this->view->data['error'] = 'Could not create client. Please try again.'; $this->view->data['form_data'] = $_POST; $this->view->render('clients/create', true, 'site'); } } } public function edit($id) { $client = Db::getRow("SELECT * FROM clients WHERE id = :id", [':id' => $id]); if (!$client) { http_response_code(404); $this->view->render('errors/404', true, 'site'); return; } $this->view->data['client'] = $client; $this->view->data['page_title'] = 'Edit ' . $client['name']; $this->view->render('clients/edit', true, 'site'); } public function update($id) { try { // Validate if (!Validator::required($_POST['name'])) { throw new Exception('Name is required'); } // Update Db::update('clients', [ 'name' => $_POST['name'], 'email' => $_POST['email'], 'phone' => $_POST['phone'] ?? null, 'updated_at' => date('Y-m-d H:i:s') ], 'id = :id', [':id' => $id]); redirect('/clients/show/' . $id); } catch (Exception $e) { if (DEBUG) { echo $e->getMessage(); } else { $this->view->data['error'] = 'Could not update client. Please try again.'; $this->view->data['client'] = $_POST; $this->view->data['client']['id'] = $id; $this->view->render('clients/edit', true, 'site'); } } } public function delete($id) { try { Db::delete('clients', 'id = :id', [':id' => $id]); redirect('/clients'); } catch (Exception $e) { if (DEBUG) { echo $e->getMessage(); } else { redirect('/clients/show/' . $id . '?error=delete_failed'); } } } } ``` ### Step 2: Create the Views Create view files at `public/views/clients/`: **Index View (`public/views/clients/index.php`):** ```php

data['page_title']; ?>

Add New Client data['clients'])): ?>

No clients found.

data['clients'] as $client): ?>
Name Email Phone Actions
View Edit
``` **Show View (`public/views/clients/show.php`):** ```php

data['client']['name']); ?>

Email: data['client']['email']); ?>

Phone: data['client']['phone'] ?? '-'); ?>

Edit Back to List

Orders

data['orders'])): ?>

No orders yet.

data['orders'] as $order): ?>
Order ID Total Date
# $
``` **Create/Edit View (`public/views/clients/create.php`):** ```php

data['page_title']; ?>

data['error'])): ?>
data['error']); ?>
Cancel
``` ### Step 3: Create Page-Specific Assets Create the corresponding CSS and JavaScript files for this page. * `public/assets/css/clients.css` * `public/assets/js/clients.js` These will be loaded by the site's wrapper. ### Step 4: Access Your New Page You can now access your new page by navigating to: - `http://yoursite.com/clients` - List all clients - `http://yoursite.com/clients/show/1` - View client #1 - `http://yoursite.com/clients/create` - Create new client form - `http://yoursite.com/clients/edit/1` - Edit client #1 The auto-routing will automatically map these URLs to the corresponding methods in your `Clients` controller. ## Tutorial: Creating a New API Endpoint This tutorial shows how to create API endpoints for product management. ### Step 1: Create the API Controller Create a new file at `api/controllers/Products.php`. ```php true, 'data' => $products, 'count' => count($products) ]); } catch (Exception $e) { http_response_code(500); echo json_encode([ 'success' => false, 'error' => DEBUG ? $e->getMessage() : 'Internal server error' ]); } } /** * GET /api/products/get/{id} * Get a single product by ID */ public function get($id) { header('Content-Type: application/json'); try { $product = Db::getRow("SELECT * FROM products WHERE id = :id", [':id' => $id]); if ($product) { echo json_encode([ 'success' => true, 'data' => $product ]); } else { http_response_code(404); echo json_encode([ 'success' => false, 'error' => 'Product not found' ]); } } catch (Exception $e) { http_response_code(500); echo json_encode([ 'success' => false, 'error' => DEBUG ? $e->getMessage() : 'Internal server error' ]); } } /** * POST /api/products/create * Create a new product */ public function create() { header('Content-Type: application/json'); try { // Get JSON input $input = json_decode(file_get_contents('php://input'), true); // Validate if (!isset($input['name']) || !isset($input['price'])) { http_response_code(400); echo json_encode([ 'success' => false, 'error' => 'Name and price are required' ]); return; } // Insert $productId = Db::insert('products', [ 'name' => $input['name'], 'description' => $input['description'] ?? null, 'price' => $input['price'], 'category' => $input['category'] ?? null, 'created_at' => date('Y-m-d H:i:s') ]); http_response_code(201); echo json_encode([ 'success' => true, 'data' => ['id' => $productId] ]); } catch (Exception $e) { http_response_code(500); echo json_encode([ 'success' => false, 'error' => DEBUG ? $e->getMessage() : 'Internal server error' ]); } } /** * PUT /api/products/update/{id} * Update an existing product */ public function update($id) { header('Content-Type: application/json'); try { // Check if product exists $product = Db::getRow("SELECT * FROM products WHERE id = :id", [':id' => $id]); if (!$product) { http_response_code(404); echo json_encode([ 'success' => false, 'error' => 'Product not found' ]); return; } // Get JSON input $input = json_decode(file_get_contents('php://input'), true); // Build update data $updateData = []; if (isset($input['name'])) $updateData['name'] = $input['name']; if (isset($input['description'])) $updateData['description'] = $input['description']; if (isset($input['price'])) $updateData['price'] = $input['price']; if (isset($input['category'])) $updateData['category'] = $input['category']; $updateData['updated_at'] = date('Y-m-d H:i:s'); // Update Db::update('products', $updateData, 'id = :id', [':id' => $id]); echo json_encode([ 'success' => true, 'data' => ['id' => $id] ]); } catch (Exception $e) { http_response_code(500); echo json_encode([ 'success' => false, 'error' => DEBUG ? $e->getMessage() : 'Internal server error' ]); } } /** * DELETE /api/products/delete/{id} * Delete a product */ public function delete($id) { header('Content-Type: application/json'); try { // Check if product exists $product = Db::getRow("SELECT * FROM products WHERE id = :id", [':id' => $id]); if (!$product) { http_response_code(404); echo json_encode([ 'success' => false, 'error' => 'Product not found' ]); return; } // Delete Db::delete('products', 'id = :id', [':id' => $id]); echo json_encode([ 'success' => true, 'data' => ['id' => $id] ]); } catch (Exception $e) { http_response_code(500); echo json_encode([ 'success' => false, 'error' => DEBUG ? $e->getMessage() : 'Internal server error' ]); } } } ``` ### Step 2: Access the API Endpoints You can now make requests to these endpoints: **Example using cURL:** ```sh # List all products curl http://yoursite.com/api/products/list # List products with filtering curl "http://yoursite.com/api/products/list?category=electronics&limit=10" # Get product with ID 123 curl http://yoursite.com/api/products/get/123 # Create a new product curl -X POST http://yoursite.com/api/products/create \ -H "Content-Type: application/json" \ -d '{"name":"New Product","price":29.99,"category":"electronics"}' # Update product curl -X PUT http://yoursite.com/api/products/update/123 \ -H "Content-Type: application/json" \ -d '{"price":24.99}' # Delete product curl -X DELETE http://yoursite.com/api/products/delete/123 ``` If `SECUREAPI` is enabled in `config.php`, you must include the authorization token in the header: ```sh # Get the authorization key from the AUTHORIZATION define in config.php API_KEY="your_secret_api_key" # Example with authorization curl -H "Authorization: $API_KEY" http://yoursite.com/api/products/get/123 ``` **Example using JavaScript (Fetch API):** ```javascript // GET request fetch('/api/products/list?category=electronics') .then(response => response.json()) .then(data => console.log(data)); // POST request fetch('/api/products/create', { method: 'POST', headers: { 'Content-Type': 'application/json', // Include if SECUREAPI is enabled // 'Authorization': 'your_secret_api_key' }, body: JSON.stringify({ name: 'New Product', price: 29.99, category: 'electronics' }) }) .then(response => response.json()) .then(data => console.log(data)); ``` ## Tutorial: Creating and Running a CLI Command This tutorial shows how to create command-line tasks for various purposes. ### Step 1: Create the Command File **Simple Command (`commands/SyncDataCommand.php`):** ```php $record['id']] ); if ($exists && !$forceSync) { echo "Skipping existing record: {$record['id']}\n"; continue; } if ($exists) { // Update existing Db::update('synced_data', [ 'data' => json_encode($record), 'synced_at' => date('Y-m-d H:i:s') ], 'external_id = :id', [':id' => $record['id']]); echo "Updated record: {$record['id']}\n"; } else { // Insert new Db::insert('synced_data', [ 'external_id' => $record['id'], 'data' => json_encode($record), 'synced_at' => date('Y-m-d H:i:s') ]); echo "Inserted record: {$record['id']}\n"; } } echo "\nData sync completed successfully.\n"; } catch (Exception $e) { echo "ERROR: " . $e->getMessage() . "\n"; exit(1); } ``` **Email Queue Processor (`commands/ProcessEmailQueueCommand.php`):** ```php (int)$limit] ); echo "Found " . count($emails) . " pending emails\n"; $sent = 0; $failed = 0; foreach ($emails as $email) { try { // Send email using your EmailService EmailService::send( $email['to'], $email['subject'], $email['body'] ); // Update status Db::update('email_queue', [ 'status' => 'sent', 'sent_at' => date('Y-m-d H:i:s') ], 'id = :id', [':id' => $email['id']]); $sent++; echo "."; } catch (Exception $e) { // Mark as failed Db::update('email_queue', [ 'status' => 'failed', 'error_message' => $e->getMessage(), 'failed_at' => date('Y-m-d H:i:s') ], 'id = :id', [':id' => $email['id']]); $failed++; echo "F"; } } echo "\n\nResults:\n"; echo "Sent: $sent\n"; echo "Failed: $failed\n"; } catch (Exception $e) { echo "ERROR: " . $e->getMessage() . "\n"; exit(1); } ``` **Database Cleanup Command (`commands/CleanupDatabaseCommand.php`):** ```php $cutoffDate] ); echo "Logs to delete: " . $oldLogs[0]['count'] . "\n"; if (!$dryRun) { $deleted = Db::delete('logs', 'created_at < :date', [':date' => $cutoffDate]); echo "Deleted: $deleted logs\n"; } // Clean soft-deleted records $oldDeleted = Db::select("SELECT COUNT(*) as count FROM clients WHERE deleted_at IS NOT NULL AND deleted_at < :date", [':date' => $cutoffDate] ); echo "\nSoft-deleted clients to remove: " . $oldDeleted[0]['count'] . "\n"; if (!$dryRun) { $deleted = Db::delete('clients', 'deleted_at IS NOT NULL AND deleted_at < :date', [':date' => $cutoffDate] ); echo "Deleted: $deleted clients\n"; } echo "\nCleanup completed.\n"; } catch (Exception $e) { echo "ERROR: " . $e->getMessage() . "\n"; exit(1); } ``` ### Step 2: Run the Commands Execute commands from the root directory of your project using the `console` entry point. ```sh # Run data sync php console SyncDataCommand # Run with options php console SyncDataCommand --date="2024-10-28" --force # Process email queue php console ProcessEmailQueueCommand --limit=50 # Database cleanup (dry run first) php console CleanupDatabaseCommand --days=60 --dry-run # Then run for real php console CleanupDatabaseCommand --days=60 ``` ### Step 3: Set Up Cron Jobs Add these to your crontab for automated execution: ```sh # Edit crontab crontab -e # Add these lines: # Process email queue every 5 minutes */5 * * * * cd /www/wwwroot/(project_directory) && php console ProcessEmailQueueCommand >> /var/log/email-queue.log 2>&1 # Sync data every hour 0 * * * * cd /www/wwwroot/(project_directory) && php console SyncDataCommand >> /var/log/data-sync.log 2>&1 # Cleanup database every night at 2 AM 0 2 * * * cd /www/wwwroot/(project_directory) && php console CleanupDatabaseCommand --days=30 >> /var/log/cleanup.log 2>&1 ``` ## Error Handling Patterns ### Controller Error Handling ```php validateClientData($_POST); // Insert client $clientId = Db::insert('clients', [ 'name' => $_POST['name'], 'email' => $_POST['email'] ]); // Insert related data Db::insert('client_metadata', [ 'client_id' => $clientId, 'source' => $_POST['source'] ?? 'website' ]); // Commit transaction Db::commit(); // Send notification email try { EmailService::sendWelcomeEmail($_POST['email'], $_POST['name']); } catch (Exception $e) { // Log email failure but don't fail the whole operation error_log("Failed to send welcome email: " . $e->getMessage()); } redirect('/clients/show/' . $clientId); } catch (ValidationException $e) { // Validation errors - show to user Db::rollback(); $this->view->data['error'] = $e->getMessage(); $this->view->data['form_data'] = $_POST; $this->view->render('clients/create', true, 'site'); } catch (Exception $e) { // Other errors Db::rollback(); if (DEBUG) { // Show full error in development echo "
";
                echo "Error: " . $e->getMessage() . "\n";
                echo "File: " . $e->getFile() . "\n";
                echo "Line: " . $e->getLine() . "\n";
                echo "\nStack Trace:\n" . $e->getTraceAsString();
                echo "
"; } else { // Generic error in production $this->view->data['error'] = 'An error occurred. Please try again.'; $this->view->data['form_data'] = $_POST; $this->view->render('clients/create', true, 'site'); // Log the error error_log("Client creation failed: " . $e->getMessage()); } } } private function validateClientData($data) { if (!Validator::required($data['name'] ?? '')) { throw new ValidationException('Name is required'); } if (!Validator::email($data['email'] ?? '')) { throw new ValidationException('Valid email is required'); } // Check for duplicate email $existing = Db::getRow("SELECT id FROM clients WHERE email = :email", [':email' => $data['email']] ); if ($existing) { throw new ValidationException('A client with this email already exists'); } } } // Custom exception for validation errors class ValidationException extends Exception {} ``` ### API Error Handling ```php validateProduct($input); $productId = Db::insert('products', [ 'name' => $input['name'], 'price' => $input['price'], 'created_at' => date('Y-m-d H:i:s') ]); http_response_code(201); echo json_encode([ 'success' => true, 'data' => ['id' => $productId] ]); } catch (ApiException $e) { http_response_code($e->getCode()); echo json_encode([ 'success' => false, 'error' => $e->getMessage() ]); } catch (Exception $e) { http_response_code(500); echo json_encode([ 'success' => false, 'error' => DEBUG ? $e->getMessage() : 'Internal server error' ]); error_log("API Error: " . $e->getMessage()); } } private function validateProduct($data) { if (!isset($data['name']) || empty(trim($data['name']))) { throw new ApiException('Product name is required', 400); } if (!isset($data['price']) || !is_numeric($data['price'])) { throw new ApiException('Valid price is required', 400); } if ($data['price'] < 0) { throw new ApiException('Price cannot be negative', 400); } } } // Custom exception for API errors class ApiException extends Exception { public function __construct($message, $code = 400) { parent::__construct($message, $code); } } ``` ### Command Error Handling ```php $order['id'], 'error' => $e->getMessage() ]; echo "E"; } } echo "\n\n"; echo "Processed: $processed\n"; echo "Errors: " . count($errors) . "\n"; if (!empty($errors)) { echo "\nFailed Orders:\n"; foreach ($errors as $error) { echo "Order #{$error['order_id']}: {$error['error']}\n"; } exit(1); // Exit with error code } } catch (Exception $e) { echo "\nFATAL ERROR: " . $e->getMessage() . "\n"; exit(1); } function processOrder($order) { // Order processing logic // Throw exceptions on failures } ``` # Common Configurations & Features ## `config.php` - Framework Configuration The `config.php` file is the central hub for all framework-wide configurations. It handles security-related keys, API keys, debugging settings, and various integration parameters. ### Security Configurations It is **highly recommended** to change all security-related values for each new project. * **`define('DEBUG', false);`**: Set to `true` to enable error reporting for development. **Always set to `false` in production.** * **`define('SECUREAPI', false);`**: Set to `true` to require an authorization key for API access. * **`define('AUTHORIZATION', 'your_secret_api_key');`**: The secret key required when `SECUREAPI` is true. **Always change this to a strong, random value.** **Example: Generating a secure API key** ```php // Generate a random API key define('AUTHORIZATION', bin2hex(random_bytes(32))); ``` ### Database Configuration ```php // Database settings define('DB_HOST', 'localhost'); define('DB_NAME', 'your_database'); define('DB_USER', 'your_username'); define('DB_PASS', 'your_password'); define('DB_CHARSET', 'utf8mb4'); ``` ### API Keys and Integrations All third-party API keys (Stripe, PHPMailer, etc.) are managed in this file, making them globally accessible via their defined constants. ```php // Stripe define('STRIPE_SECRET_KEY', 'sk_test_...'); define('STRIPE_PUBLIC_KEY', 'pk_test_...'); // Email (PHPMailer) define('SMTP_HOST', 'smtp.gmail.com'); define('SMTP_PORT', 587); define('SMTP_USER', 'your-email@gmail.com'); define('SMTP_PASS', 'your-app-password'); define('SMTP_FROM', 'noreply@yoursite.com'); define('SMTP_FROM_NAME', 'Your Site Name'); // AWS S3 define('AWS_KEY', 'your-aws-key'); define('AWS_SECRET', 'your-aws-secret'); define('AWS_REGION', 'us-east-1'); define('AWS_BUCKET', 'your-bucket-name'); // Other services define('RECAPTCHA_SITE_KEY', 'your-site-key'); define('RECAPTCHA_SECRET_KEY', 'your-secret-key'); ``` ### Path Constants ```php // Define base paths define('ROOT_PATH', __DIR__); define('APP_PATH', ROOT_PATH . '/app'); define('PUBLIC_PATH', ROOT_PATH . '/public'); define('TEMPLATES_PATH', ROOT_PATH . '/templates'); define('UPLOADS_PATH', PUBLIC_PATH . '/uploads'); ``` ### Environment-Specific Configuration ```php // Determine environment define('ENVIRONMENT', getenv('APP_ENV') ?: 'production'); // Environment-specific settings if (ENVIRONMENT === 'development') { define('DEBUG', true); define('BASE_URL', 'http://localhost:8000'); } else { define('DEBUG', false); define('BASE_URL', 'https://yoursite.com'); } ``` ## .htaccess and SSL The root `.htaccess` file includes a rule for automatic redirection to HTTPS. It is recommended to temporarily disable this rule when setting up a new SSL certificate (e.g., with Let's Encrypt) to avoid issues with the verification process. ```apache # Force HTTPS # Comment out these lines when setting up SSL certificate RewriteCond %{HTTPS} off RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301] # Standard routing RewriteEngine On RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule ^(.*)$ index.php [QSA,L] ``` ## Public Assets Structure The `public/assets` directory is organized to promote performance and streamline debugging by using page-specific CSS and JavaScript files. ``` public/assets/ ├── css/ │ ├── global.css # Site-wide styles │ ├── clients.css # Page-specific: clients │ ├── products.css # Page-specific: products │ └── dashboard.css # Page-specific: dashboard ├── js/ │ ├── global.js # Site-wide scripts │ ├── clients.js # Page-specific: clients │ ├── products.js # Page-specific: products │ └── dashboard.js # Page-specific: dashboard └── images/ ├── logo.png └── icons/ ``` **Wrapper automatically loads page-specific assets:** ```php getControllerName()); if (file_exists(PUBLIC_PATH . "/assets/css/{$controller}.css")) { echo ""; } ?> ``` ## View System ### Wrappers (Header & Footer) A "wrapper" is the main site template (`header.php` and `footer.php`). You can control which wrapper to use or disable it entirely, which is useful for pages like a login screen. **Wrapper Location:** ``` templates/wrappers/ ├── site/ # Main site wrapper │ ├── header.php │ └── footer.php ├── admin/ # Admin panel wrapper │ ├── header.php │ └── footer.php └── minimal/ # Minimal wrapper (login, etc.) ├── header.php └── footer.php ``` **Usage Examples:** ```php // Use default 'site' wrapper $this->view->render('clients/index', true, 'site'); // Use admin wrapper $this->view->render('admin/dashboard', true, 'admin'); // Use minimal wrapper (login page) $this->view->render('auth/login', true, 'minimal'); // No wrapper at all $this->view->render('auth/login', false); ``` **Example Wrapper (`templates/wrappers/site/header.php`):** ```php <?php echo $this->data['page_title'] ?? 'My Site'; ?> getControllerName()); if (file_exists(PUBLIC_PATH . "/assets/css/{$controller}.css")) { echo ""; } ?>
``` **Example Footer (`templates/wrappers/site/footer.php`):** ```php
getControllerName()); if (file_exists(PUBLIC_PATH . "/assets/js/{$controller}.js")) { echo ""; } ?> ``` ### Partial Views Partial views allow you to load reusable HTML snippets (like sidebars or breadcrumbs) into other views, similar to `require_once()`. **Partial Location:** ``` public/views/ ├── _partials/ │ ├── breadcrumb.php │ ├── sidebar.php │ ├── pagination.php │ └── client_card.php ``` **Usage in Views:** ```php
partial('_partials/breadcrumb', [ 'Home' => '/', 'Clients' => '/clients', $this->data['client']['name'] => '' ]); ?>

data['client']['name']); ?>

partial('_partials/sidebar'); ?>
``` **Example Partial (`public/views/_partials/breadcrumb.php`):** ```php ``` ## Routing The framework uses auto-routing by convention (`/controller/method`) for most cases. For more complex needs, it integrates `AltoRouter`. ### Auto-Routing Examples ``` URL: /clients Maps to: Clients::index() URL: /clients/show/123 Maps to: Clients::show(123) URL: /products/edit/456 Maps to: Products::edit(456) ``` ### AltoRouter for Complex Routes Define custom routes in `public/routes.php` or `api/routes.php`: ```php map('GET', '/users/[i:id]', 'Users#show', 'user_show'); // Multiple parameters $router->map('GET', '/blog/[i:year]/[i:month]/[*:slug]', 'Blog#show', 'blog_post'); // POST route $router->map('POST', '/api/users/create', 'UsersAPI#create', 'api_user_create'); // Optional parameter $router->map('GET', '/products/category/[a:category]?', 'Products#category', 'products_category'); ``` **Match Types:** * **`[i:id]`**: Match an integer as parameter 'id' * **`[a:action]`**: Match alphanumeric characters (A-Z, a-z, 0-9, -) * **`[h:key]`**: Match hexadecimal characters * **`[*:trailing]`**: Catch-all for the rest of the URL path * **`[**:path]`**: Match everything including slashes **Using Named Routes:** ```php // Generate URL for a named route echo $router->generate('user_show', ['id' => 5]); // Outputs: /users/5 echo $router->generate('blog_post', [ 'year' => 2024, 'month' => 10, 'slug' => 'my-first-post' ]); // Outputs: /blog/2024/10/my-first-post ``` **In Controllers:** ```php $id]); $this->view->data['user'] = $user; $this->view->render('users/show', true, 'site'); } } ``` ## `templates/` - Reusable HTML Documents This directory holds reusable HTML documents, such as transactional emails (`/templates/emails/`). These templates use placeholders like `%FNAME%` that are replaced with dynamic data by PHP before being sent. **Template Structure:** ``` templates/ ├── emails/ │ ├── welcome.html │ ├── password_reset.html │ ├── invoice.html │ └── notification.html └── wrappers/ ├── site/ ├── admin/ └── minimal/ ``` **Example Email Template (`templates/emails/welcome.html`):** ```html

Welcome to %SITE_NAME%

Hi %FNAME%,

Thank you for signing up! We're excited to have you on board.

Your account email is: %EMAIL%

Verify Your Email

Thanks,
The %SITE_NAME% Team

``` **Using Templates in Code:** ```php $firstName, '%EMAIL%' => $email, '%SITE_NAME%' => 'My Awesome Site', '%VERIFY_LINK%' => BASE_URL . '/verify?token=' . self::generateToken($email) ]; $body = str_replace( array_keys($replacements), array_values($replacements), $template ); // Send email (using PHPMailer or other) return self::send($email, 'Welcome to My Awesome Site', $body); } private static function send($to, $subject, $body) { // PHPMailer implementation $mail = new PHPMailer(true); try { $mail->isSMTP(); $mail->Host = SMTP_HOST; $mail->SMTPAuth = true; $mail->Username = SMTP_USER; $mail->Password = SMTP_PASS; $mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS; $mail->Port = SMTP_PORT; $mail->setFrom(SMTP_FROM, SMTP_FROM_NAME); $mail->addAddress($to); $mail->isHTML(true); $mail->Subject = $subject; $mail->Body = $body; $mail->send(); return true; } catch (Exception $e) { error_log("Email send failed: " . $mail->ErrorInfo); return false; } } } ``` ## `plugins/` - Extendable Functionality (In Development) This directory is for modular, self-contained add-ons (like a CRM or weather widget) that can extend the framework's functionality, similar to WordPress plugins. **Planned Structure:** ``` plugins/ ├── crm/ │ ├── plugin.php # Plugin initialization │ ├── controllers/ │ ├── views/ │ ├── assets/ │ └── README.md ├── weather-widget/ │ ├── plugin.php │ ├── WeatherWidget.php │ └── assets/ └── analytics/ ├── plugin.php ├── services/ └── config.php ``` # Core Classes Reference ## Controller (`core/Controller.php`) Base controller class that all public controllers extend. **Properties:** - `$this->view` - View renderer instance **Methods:** - `redirect($url)` - Perform HTTP redirect - `getControllerName()` - Get current controller name - `getMethodName()` - Get current method name **Example:** ```php data in the view $this->view->data['clients'] = Db::select("SELECT * FROM clients"); $this->view->render('clients/index', true, 'site'); } protected function requireAuth() { if (!Auth::isLoggedIn()) { $this->redirect('/login'); } } } ``` ## View (`core/View.php`) Handles view rendering and data passing. **Properties:** - `$this->data` - Array of variables to pass to view **Methods:** - `render($viewPath, $useWrapper = true, $wrapperName = 'site')` - Render a view - `partial($partialPath, $data = [])` - Load a partial view - `getControllerName()` - Get current controller name (for asset loading) **Example:** ```php // In controller $this->view->data['title'] = 'My Page'; $this->view->data['users'] = $users; $this->view->render('users/index', true, 'site'); // In view

data['title']; ?>

data['users'] as $user): ?>

// Using partials partial('_partials/breadcrumb', [ 'breadcrumb' => ['Home' => '/', 'Users' => ''] ]); ?> ``` ## Db (`app/Helpers/DB.php`) Static database helper class for all database operations. **Connection Methods:** - `Db::getConnection()` - Get PDO connection instance - `Db::beginTransaction()` - Start transaction - `Db::commit()` - Commit transaction - `Db::rollback()` - Rollback transaction **Query Methods:** - `Db::select($query, $params = [])` - Execute SELECT and return all rows - `Db::getRow($query, $params = [])` - Execute SELECT and return first row - `Db::execute($query, $params = [])` - Execute any query - `Db::insert($table, $data)` - Insert row and return last insert ID - `Db::update($table, $data, $where, $whereParams = [])` - Update rows and return affected count - `Db::delete($table, $where, $whereParams = [])` - Delete rows and return affected count **Examples:** ```php // Select multiple rows $clients = Db::select("SELECT * FROM clients WHERE active = :active", [':active' => 1]); // Select single row $client = Db::getRow("SELECT * FROM clients WHERE id = :id", [':id' => $id]); // Insert $clientId = Db::insert('clients', [ 'name' => 'John Doe', 'email' => 'john@example.com', 'created_at' => date('Y-m-d H:i:s') ]); // Update $affected = Db::update('clients', ['name' => 'Jane Doe', 'updated_at' => date('Y-m-d H:i:s')], 'id = :id', [':id' => $id] ); // Delete $affected = Db::delete('clients', 'id = :id', [':id' => $id]); // Custom query Db::execute("UPDATE clients SET last_login = NOW() WHERE id = :id", [':id' => $id]); // Transactions Db::beginTransaction(); try { Db::insert('orders', ['client_id' => 1, 'total' => 100]); Db::update('clients', ['balance' => 'balance - 100'], 'id = 1'); Db::commit(); } catch (Exception $e) { Db::rollback(); throw $e; } ``` ## Helper Classes ### Validator (`app/Helpers/Validator.php`) Input validation helper. **Methods:** ```php Validator::required($value) // Check if not empty Validator::email($value) // Validate email format Validator::minLength($value, $min) // Check minimum length Validator::maxLength($value, $max) // Check maximum length Validator::numeric($value) // Check if numeric Validator::alpha($value) // Check if alphabetic Validator::alphanumeric($value) // Check if alphanumeric Validator::url($value) // Validate URL format Validator::date($value) // Validate date format ``` ### Session (`app/Helpers/Session.php`) Session management helper. **Methods:** ```php Session::start() // Start session Session::set($key, $value) // Set session variable Session::get($key, $default = null) // Get session variable Session::has($key) // Check if key exists Session::delete($key) // Delete session variable Session::destroy() // Destroy session Session::flash($key, $value) // Set flash message Session::getFlash($key) // Get and delete flash message ``` ### Auth (`app/Helpers/Auth.php`) Authentication helper. **Methods:** ```php Auth::login($userId) // Log in user Auth::logout() // Log out user Auth::isLoggedIn() // Check if user is logged in Auth::getUserId() // Get current user ID Auth::getUser() // Get current user data Auth::check($permission) // Check user permission ``` # Database Usage The `Db` helper class (`app/Helpers/DB.php`) provides a static interface for all database interactions. All queries use prepared statements for security. ## Basic Queries ### SELECT - Multiple Rows ```php // Simple select $clients = Db::select("SELECT * FROM clients"); // With WHERE clause $activeClients = Db::select( "SELECT * FROM clients WHERE active = :active", [':active' => 1] ); // With multiple conditions $clients = Db::select( "SELECT * FROM clients WHERE status = :status AND created_at > :date ORDER BY name", [':status' => 'active', ':date' => '2024-01-01'] ); // With JOIN $orders = Db::select( "SELECT o.*, c.name as client_name FROM orders o JOIN clients c ON o.client_id = c.id WHERE o.status = :status", [':status' => 'pending'] ); ``` ### SELECT - Single Row ```php // Get single row $client = Db::getRow("SELECT * FROM clients WHERE id = :id", [':id' => $id]); // Check if exists if ($client) { echo $client['name']; } else { echo "Client not found"; } // Get specific columns $email = Db::getRow("SELECT email FROM clients WHERE id = :id", [':id' => $id]); ``` ## INSERT Operations ```php // Simple insert $clientId = Db::insert('clients', [ 'name' => 'John Doe', 'email' => 'john@example.com', 'phone' => '555-1234' ]); // Insert with timestamp $clientId = Db::insert('clients', [ 'name' => 'Jane Doe', 'email' => 'jane@example.com', 'created_at' => date('Y-m-d H:i:s'), 'updated_at' => date('Y-m-d H:i:s') ]); // Insert and use the ID $orderId = Db::insert('orders', [ 'client_id' => $clientId, 'total' => 99.99, 'status' => 'pending' ]); ``` ## UPDATE Operations ```php // Update single record $affected = Db::update('clients', ['name' => 'John Smith', 'updated_at' => date('Y-m-d H:i:s')], 'id = :id', [':id' => $id] ); // Update multiple records $affected = Db::update('orders', ['status' => 'shipped', 'shipped_at' => date('Y-m-d H:i:s')], 'status = :old_status AND created_at < :date', [':old_status' => 'pending', ':date' => '2024-01-01'] ); // Increment value Db::execute( "UPDATE products SET view_count = view_count + 1 WHERE id = :id", [':id' => $productId] ); ``` ## DELETE Operations ```php // Delete single record $affected = Db::delete('clients', 'id = :id', [':id' => $id]); // Delete multiple records $affected = Db::delete('logs', 'created_at < :date', [':date' => '2024-01-01']); // Soft delete (recommended) Db::update('clients', ['deleted_at' => date('Y-m-d H:i:s')], 'id = :id', [':id' => $id] ); ``` ## Transactions Use transactions for operations that must all succeed or all fail together. ```php // Simple transaction Db::beginTransaction(); try { // Create order $orderId = Db::insert('orders', [ 'client_id' => $clientId, 'total' => 100.00, 'status' => 'pending' ]); // Deduct from client balance Db::execute( "UPDATE clients SET balance = balance - :amount WHERE id = :id", [':amount' => 100.00, ':id' => $clientId] ); // Insert order items foreach ($items as $item) { Db::insert('order_items', [ 'order_id' => $orderId, 'product_id' => $item['product_id'], 'quantity' => $item['quantity'], 'price' => $item['price'] ]); } Db::commit(); echo "Order created successfully"; } catch (Exception $e) { Db::rollback(); echo "Order failed: " . $e->getMessage(); } ``` ## Complex Queries ### Aggregation ```php // COUNT $count = Db::getRow("SELECT COUNT(*) as total FROM clients WHERE active = 1"); echo $count['total']; // SUM, AVG, etc. $stats = Db::getRow(" SELECT COUNT(*) as order_count, SUM(total) as revenue, AVG(total) as avg_order FROM orders WHERE status = 'completed' "); ``` ### Subqueries ```php $clients = Db::select(" SELECT c.*, (SELECT COUNT(*) FROM orders WHERE client_id = c.id) as order_count, (SELECT SUM(total) FROM orders WHERE client_id = c.id) as total_spent FROM clients c WHERE c.active = 1 "); ``` ### GROUP BY ```php $monthlySales = Db::select(" SELECT DATE_FORMAT(created_at, '%Y-%m') as month, COUNT(*) as order_count, SUM(total) as revenue FROM orders WHERE status = 'completed' GROUP BY DATE_FORMAT(created_at, '%Y-%m') ORDER BY month DESC "); ``` ### CASE Statements ```php $clients = Db::select(" SELECT name, CASE WHEN total_spent > 10000 THEN 'VIP' WHEN total_spent > 5000 THEN 'Premium' ELSE 'Standard' END as tier FROM clients "); ``` ## Raw Queries with execute() For queries that don't fit insert/update/delete patterns: ```php // Custom UPDATE with calculations Db::execute(" UPDATE products SET discount_price = price * 0.9 WHERE category = :category", [':category' => 'electronics'] ); // Batch operations Db::execute(" INSERT INTO archive_clients SELECT * FROM clients WHERE deleted_at < :date", [':date' => date('Y-m-d', strtotime('-1 year'))] ); ``` ## Performance Tips ### Use LIMIT for Large Datasets ```php // Paginated results $page = $_GET['page'] ?? 1; $perPage = 20; $offset = ($page - 1) * $perPage; $clients = Db::select( "SELECT * FROM clients LIMIT :limit OFFSET :offset", [':limit' => $perPage, ':offset' => $offset] ); ``` ### Index Your Queries ```sql -- Add indexes for frequently queried columns CREATE INDEX idx_clients_email ON clients(email); CREATE INDEX idx_orders_status ON orders(status); CREATE INDEX idx_orders_client_id ON orders(client_id); ``` ### Use EXISTS Instead of COUNT ```php // Slow $exists = Db::getRow("SELECT COUNT(*) as c FROM clients WHERE email = :email", [':email' => $email]); if ($exists['c'] > 0) { } // Fast $exists = Db::getRow("SELECT 1 FROM clients WHERE email = :email LIMIT 1", [':email' => $email]); if ($exists) { } ``` # Common Real-World Scenarios ## File Upload Handling ```php $maxSize) { throw new Exception('File too large (max 5MB)'); } $allowedTypes = ['application/pdf', 'image/jpeg', 'image/png']; if (!in_array($file['type'], $allowedTypes)) { throw new Exception('Invalid file type'); } // Generate unique filename $ext = pathinfo($file['name'], PATHINFO_EXTENSION); $filename = uniqid() . '_' . time() . '.' . $ext; $uploadPath = UPLOADS_PATH . '/documents/' . $filename; // Move file if (!move_uploaded_file($file['tmp_name'], $uploadPath)) { throw new Exception('Failed to save file'); } // Save to database $docId = Db::insert('documents', [ 'user_id' => Auth::getUserId(), 'filename' => $filename, 'original_name' => $file['name'], 'file_size' => $file['size'], 'mime_type' => $file['type'], 'uploaded_at' => date('Y-m-d H:i:s') ]); redirect('/documents/show/' . $docId); } catch (Exception $e) { $this->view->data['error'] = $e->getMessage(); $this->view->render('documents/upload', true, 'site'); } } } ``` ## Pagination ```php $perPage, ':offset' => $offset] ); $this->view->data['products'] = $products; $this->view->data['current_page'] = $page; $this->view->data['total_pages'] = $totalPages; $this->view->render('products/index', true, 'site'); } } ``` ## Search with Filters ```php = :date_from"; $params[':date_from'] = $_GET['date_from']; } if (!empty($_GET['date_to'])) { $query .= " AND created_at <= :date_to"; $params[':date_to'] = $_GET['date_to'] . ' 23:59:59'; } $query .= " ORDER BY created_at DESC"; $clients = Db::select($query, $params); $this->view->data['clients'] = $clients; $this->view->data['filters'] = $_GET; $this->view->render('clients/search', true, 'site'); } } ``` ## Authentication System ```php $email]); if (!$user || !password_verify($password, $user['password'])) { throw new Exception('Invalid credentials'); } Session::set('user_id', $user['id']); Session::set('user_name', $user['name']); Session::set('user_role', $user['role']); // Update last login Db::update('users', ['last_login' => date('Y-m-d H:i:s')], 'id = :id', [':id' => $user['id']] ); return true; } public static function logout() { Session::destroy(); } public static function isLoggedIn() { return Session::has('user_id'); } public static function getUserId() { return Session::get('user_id'); } public static function getUser() { if (!self::isLoggedIn()) { return null; } return Db::getRow("SELECT * FROM users WHERE id = :id", [':id' => self::getUserId()]); } public static function requireRole($role) { if (Session::get('user_role') !== $role) { throw new Exception('Unauthorized'); } } } // Login controller class Login extends Controller { public function index() { if (Auth::isLoggedIn()) { redirect('/dashboard'); } $this->view->render('auth/login', false); } public function submit() { try { Auth::login($_POST['email'], $_POST['password']); redirect('/dashboard'); } catch (Exception $e) { $this->view->data['error'] = $e->getMessage(); $this->view->render('auth/login', false); } } public function logout() { Auth::logout(); redirect('/login'); } } ``` ## Modal Integration System Working with Modals; I have a dynamic modal intergration system. data-url is the link to the modal Modal Views are located under public/views/modals/(modal_name.php) ## Modal Structure ``` /www/wwwroot/(project_directory)/ ├── public/ ├── views/ └── modals/ ``` ```html ``` ### Building the Modal ```html ```