2393 lines
70 KiB
Markdown
2393 lines
70 KiB
Markdown
|
|
# 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
|
||
|
|
<?php
|
||
|
|
// app/Helpers/Validator.php
|
||
|
|
class Validator {
|
||
|
|
public static function email($value) {
|
||
|
|
return filter_var($value, FILTER_VALIDATE_EMAIL) !== false;
|
||
|
|
}
|
||
|
|
|
||
|
|
public static function required($value) {
|
||
|
|
return !empty(trim($value));
|
||
|
|
}
|
||
|
|
|
||
|
|
public static function minLength($value, $min) {
|
||
|
|
return strlen($value) >= $min;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Example Service:**
|
||
|
|
```php
|
||
|
|
<?php
|
||
|
|
// app/Services/EmailService.php
|
||
|
|
class EmailService {
|
||
|
|
public static function sendWelcomeEmail($email, $name) {
|
||
|
|
$template = file_get_contents(TEMPLATES_PATH . '/emails/welcome.html');
|
||
|
|
$template = str_replace('%FNAME%', $name, $template);
|
||
|
|
|
||
|
|
// Use PHPMailer or your email provider
|
||
|
|
// Return true/false based on success
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Quick Reference
|
||
|
|
|
||
|
|
### File Naming Conventions
|
||
|
|
- **Controllers:** `PascalCase.php` (e.g., `ClientReports.php`)
|
||
|
|
- **Views:** `snake_case/` folders, `snake_case.php` files (e.g., `client_reports/index.php`)
|
||
|
|
- **Commands:** `PascalCaseCommand.php` (e.g., `SyncDataCommand.php`)
|
||
|
|
- **Helpers:** `PascalCase.php` (e.g., `Validator.php`)
|
||
|
|
- **Services:** `PascalCaseService.php` (e.g., `EmailService.php`)
|
||
|
|
|
||
|
|
### Common Database Patterns
|
||
|
|
```php
|
||
|
|
// Fetch all records
|
||
|
|
$clients = Db::select("SELECT * FROM clients WHERE active = :active", [':active' => 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
|
||
|
|
<?php
|
||
|
|
// public/controllers/Clients.php
|
||
|
|
class Clients extends Controller
|
||
|
|
{
|
||
|
|
public function __construct() {
|
||
|
|
parent::__construct();
|
||
|
|
// Optional: Add authentication check
|
||
|
|
// if (!Auth::isLoggedIn()) {
|
||
|
|
// redirect('/login');
|
||
|
|
// }
|
||
|
|
}
|
||
|
|
|
||
|
|
public function index()
|
||
|
|
{
|
||
|
|
// Fetch data using a helper from the app/ directory
|
||
|
|
$clients = Db::select("SELECT * FROM clients ORDER BY name ASC");
|
||
|
|
|
||
|
|
// Pass data to the view
|
||
|
|
$this->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
|
||
|
|
<div class="container">
|
||
|
|
<h1><?php echo $this->data['page_title']; ?></h1>
|
||
|
|
|
||
|
|
<a href="/clients/create" class="btn btn-primary">Add New Client</a>
|
||
|
|
|
||
|
|
<?php if (empty($this->data['clients'])): ?>
|
||
|
|
<p>No clients found.</p>
|
||
|
|
<?php else: ?>
|
||
|
|
<table class="table">
|
||
|
|
<thead>
|
||
|
|
<tr>
|
||
|
|
<th>Name</th>
|
||
|
|
<th>Email</th>
|
||
|
|
<th>Phone</th>
|
||
|
|
<th>Actions</th>
|
||
|
|
</tr>
|
||
|
|
</thead>
|
||
|
|
<tbody>
|
||
|
|
<?php foreach ($this->data['clients'] as $client): ?>
|
||
|
|
<tr>
|
||
|
|
<td><?php echo htmlspecialchars($client['name']); ?></td>
|
||
|
|
<td><?php echo htmlspecialchars($client['email']); ?></td>
|
||
|
|
<td><?php echo htmlspecialchars($client['phone'] ?? '-'); ?></td>
|
||
|
|
<td>
|
||
|
|
<a href="/clients/show/<?php echo $client['id']; ?>">View</a>
|
||
|
|
<a href="/clients/edit/<?php echo $client['id']; ?>">Edit</a>
|
||
|
|
</td>
|
||
|
|
</tr>
|
||
|
|
<?php endforeach; ?>
|
||
|
|
</tbody>
|
||
|
|
</table>
|
||
|
|
<?php endif; ?>
|
||
|
|
</div>
|
||
|
|
```
|
||
|
|
|
||
|
|
**Show View (`public/views/clients/show.php`):**
|
||
|
|
```php
|
||
|
|
<div class="container">
|
||
|
|
<h1><?php echo htmlspecialchars($this->data['client']['name']); ?></h1>
|
||
|
|
|
||
|
|
<div class="client-details">
|
||
|
|
<p><strong>Email:</strong> <?php echo htmlspecialchars($this->data['client']['email']); ?></p>
|
||
|
|
<p><strong>Phone:</strong> <?php echo htmlspecialchars($this->data['client']['phone'] ?? '-'); ?></p>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="actions">
|
||
|
|
<a href="/clients/edit/<?php echo $this->data['client']['id']; ?>" class="btn btn-primary">Edit</a>
|
||
|
|
<a href="/clients" class="btn btn-secondary">Back to List</a>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<h2>Orders</h2>
|
||
|
|
<?php if (empty($this->data['orders'])): ?>
|
||
|
|
<p>No orders yet.</p>
|
||
|
|
<?php else: ?>
|
||
|
|
<table class="table">
|
||
|
|
<thead>
|
||
|
|
<tr>
|
||
|
|
<th>Order ID</th>
|
||
|
|
<th>Total</th>
|
||
|
|
<th>Date</th>
|
||
|
|
</tr>
|
||
|
|
</thead>
|
||
|
|
<tbody>
|
||
|
|
<?php foreach ($this->data['orders'] as $order): ?>
|
||
|
|
<tr>
|
||
|
|
<td>#<?php echo $order['id']; ?></td>
|
||
|
|
<td>$<?php echo number_format($order['total'], 2); ?></td>
|
||
|
|
<td><?php echo date('M j, Y', strtotime($order['created_at'])); ?></td>
|
||
|
|
</tr>
|
||
|
|
<?php endforeach; ?>
|
||
|
|
</tbody>
|
||
|
|
</table>
|
||
|
|
<?php endif; ?>
|
||
|
|
</div>
|
||
|
|
```
|
||
|
|
|
||
|
|
**Create/Edit View (`public/views/clients/create.php`):**
|
||
|
|
```php
|
||
|
|
<div class="container">
|
||
|
|
<h1><?php echo $this->data['page_title']; ?></h1>
|
||
|
|
|
||
|
|
<?php if (isset($this->data['error'])): ?>
|
||
|
|
<div class="alert alert-danger">
|
||
|
|
<?php echo htmlspecialchars($this->data['error']); ?>
|
||
|
|
</div>
|
||
|
|
<?php endif; ?>
|
||
|
|
|
||
|
|
<form method="POST" action="/clients/store">
|
||
|
|
<div class="form-group">
|
||
|
|
<label for="name">Name *</label>
|
||
|
|
<input type="text" id="name" name="name" class="form-control"
|
||
|
|
value="<?php echo htmlspecialchars($this->data['form_data']['name'] ?? ''); ?>" required>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="form-group">
|
||
|
|
<label for="email">Email *</label>
|
||
|
|
<input type="email" id="email" name="email" class="form-control"
|
||
|
|
value="<?php echo htmlspecialchars($this->data['form_data']['email'] ?? ''); ?>" required>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="form-group">
|
||
|
|
<label for="phone">Phone</label>
|
||
|
|
<input type="tel" id="phone" name="phone" class="form-control"
|
||
|
|
value="<?php echo htmlspecialchars($this->data['form_data']['phone'] ?? ''); ?>">
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<button type="submit" class="btn btn-primary">Create Client</button>
|
||
|
|
<a href="/clients" class="btn btn-secondary">Cancel</a>
|
||
|
|
</form>
|
||
|
|
</div>
|
||
|
|
```
|
||
|
|
|
||
|
|
### 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
|
||
|
|
<?php
|
||
|
|
// api/controllers/Products.php
|
||
|
|
class Products
|
||
|
|
{
|
||
|
|
/**
|
||
|
|
* GET /api/products/list
|
||
|
|
* Get all products with optional filtering
|
||
|
|
*/
|
||
|
|
public function list()
|
||
|
|
{
|
||
|
|
header('Content-Type: application/json');
|
||
|
|
|
||
|
|
try {
|
||
|
|
// Get query parameters
|
||
|
|
$category = $_GET['category'] ?? null;
|
||
|
|
$limit = isset($_GET['limit']) ? (int)$_GET['limit'] : 50;
|
||
|
|
$offset = isset($_GET['offset']) ? (int)$_GET['offset'] : 0;
|
||
|
|
|
||
|
|
// Build query
|
||
|
|
$query = "SELECT * FROM products WHERE 1=1";
|
||
|
|
$params = [];
|
||
|
|
|
||
|
|
if ($category) {
|
||
|
|
$query .= " AND category = :category";
|
||
|
|
$params[':category'] = $category;
|
||
|
|
}
|
||
|
|
|
||
|
|
$query .= " LIMIT :limit OFFSET :offset";
|
||
|
|
$params[':limit'] = $limit;
|
||
|
|
$params[':offset'] = $offset;
|
||
|
|
|
||
|
|
$products = Db::select($query, $params);
|
||
|
|
|
||
|
|
echo json_encode([
|
||
|
|
'success' => 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
|
||
|
|
<?php
|
||
|
|
// commands/SyncDataCommand.php
|
||
|
|
|
||
|
|
echo "=== Data Sync Command ===\n";
|
||
|
|
|
||
|
|
// You can use arguments passed from the command line
|
||
|
|
$options = getopt("", ["date:", "force"]);
|
||
|
|
$syncDate = $options['date'] ?? 'today';
|
||
|
|
$forceSync = isset($options['force']);
|
||
|
|
|
||
|
|
echo "Starting data sync for: " . $syncDate . "\n";
|
||
|
|
if ($forceSync) {
|
||
|
|
echo "Force mode enabled\n";
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
// Example: Sync data from external API
|
||
|
|
$externalData = file_get_contents('https://api.example.com/data');
|
||
|
|
$data = json_decode($externalData, true);
|
||
|
|
|
||
|
|
foreach ($data as $record) {
|
||
|
|
// Check if record exists
|
||
|
|
$exists = Db::getRow("SELECT id FROM synced_data WHERE external_id = :id",
|
||
|
|
[':id' => $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
|
||
|
|
<?php
|
||
|
|
// commands/ProcessEmailQueueCommand.php
|
||
|
|
|
||
|
|
echo "=== Processing Email Queue ===\n";
|
||
|
|
|
||
|
|
$options = getopt("", ["limit:"]);
|
||
|
|
$limit = $options['limit'] ?? 100;
|
||
|
|
|
||
|
|
try {
|
||
|
|
// Get pending emails
|
||
|
|
$emails = Db::select(
|
||
|
|
"SELECT * FROM email_queue WHERE status = 'pending' LIMIT :limit",
|
||
|
|
[':limit' => (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
|
||
|
|
<?php
|
||
|
|
// commands/CleanupDatabaseCommand.php
|
||
|
|
|
||
|
|
echo "=== Database Cleanup ===\n";
|
||
|
|
|
||
|
|
$options = getopt("", ["days:", "dry-run"]);
|
||
|
|
$days = $options['days'] ?? 30;
|
||
|
|
$dryRun = isset($options['dry-run']);
|
||
|
|
|
||
|
|
if ($dryRun) {
|
||
|
|
echo "DRY RUN MODE - No changes will be made\n";
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
$cutoffDate = date('Y-m-d', strtotime("-{$days} days"));
|
||
|
|
echo "Cleaning up records older than: $cutoffDate\n\n";
|
||
|
|
|
||
|
|
// Clean old logs
|
||
|
|
$oldLogs = Db::select("SELECT COUNT(*) as count FROM logs WHERE created_at < :date",
|
||
|
|
[':date' => $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
|
||
|
|
<?php
|
||
|
|
class Clients extends Controller
|
||
|
|
{
|
||
|
|
public function create()
|
||
|
|
{
|
||
|
|
try {
|
||
|
|
// Start transaction for complex operations
|
||
|
|
Db::beginTransaction();
|
||
|
|
|
||
|
|
// Validate input
|
||
|
|
$this->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 "<pre>";
|
||
|
|
echo "Error: " . $e->getMessage() . "\n";
|
||
|
|
echo "File: " . $e->getFile() . "\n";
|
||
|
|
echo "Line: " . $e->getLine() . "\n";
|
||
|
|
echo "\nStack Trace:\n" . $e->getTraceAsString();
|
||
|
|
echo "</pre>";
|
||
|
|
} 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
|
||
|
|
<?php
|
||
|
|
class Products
|
||
|
|
{
|
||
|
|
public function create()
|
||
|
|
{
|
||
|
|
header('Content-Type: application/json');
|
||
|
|
|
||
|
|
try {
|
||
|
|
$input = json_decode(file_get_contents('php://input'), true);
|
||
|
|
|
||
|
|
if (json_last_error() !== JSON_ERROR_NONE) {
|
||
|
|
throw new ApiException('Invalid JSON', 400);
|
||
|
|
}
|
||
|
|
|
||
|
|
$this->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
|
||
|
|
<?php
|
||
|
|
// commands/ProcessOrdersCommand.php
|
||
|
|
|
||
|
|
echo "=== Processing Orders ===\n";
|
||
|
|
|
||
|
|
$errors = [];
|
||
|
|
$processed = 0;
|
||
|
|
|
||
|
|
try {
|
||
|
|
$orders = Db::select("SELECT * FROM orders WHERE status = 'pending'");
|
||
|
|
|
||
|
|
foreach ($orders as $order) {
|
||
|
|
try {
|
||
|
|
// Process order
|
||
|
|
processOrder($order);
|
||
|
|
$processed++;
|
||
|
|
echo ".";
|
||
|
|
|
||
|
|
} catch (Exception $e) {
|
||
|
|
// Log individual order failure but continue processing
|
||
|
|
$errors[] = [
|
||
|
|
'order_id' => $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
|
||
|
|
<!-- In templates/wrappers/site/header.php -->
|
||
|
|
<link rel="stylesheet" href="/assets/css/global.css">
|
||
|
|
<?php
|
||
|
|
// Auto-load page-specific CSS if it exists
|
||
|
|
$controller = strtolower($this->getControllerName());
|
||
|
|
if (file_exists(PUBLIC_PATH . "/assets/css/{$controller}.css")) {
|
||
|
|
echo "<link rel='stylesheet' href='/assets/css/{$controller}.css'>";
|
||
|
|
}
|
||
|
|
?>
|
||
|
|
```
|
||
|
|
|
||
|
|
## 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
|
||
|
|
<!DOCTYPE html>
|
||
|
|
<html lang="en">
|
||
|
|
<head>
|
||
|
|
<meta charset="UTF-8">
|
||
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
|
|
<title><?php echo $this->data['page_title'] ?? 'My Site'; ?></title>
|
||
|
|
|
||
|
|
<!-- Global CSS -->
|
||
|
|
<link rel="stylesheet" href="/assets/css/global.css">
|
||
|
|
|
||
|
|
<!-- Page-specific CSS -->
|
||
|
|
<?php
|
||
|
|
$controller = strtolower($this->getControllerName());
|
||
|
|
if (file_exists(PUBLIC_PATH . "/assets/css/{$controller}.css")) {
|
||
|
|
echo "<link rel='stylesheet' href='/assets/css/{$controller}.css'>";
|
||
|
|
}
|
||
|
|
?>
|
||
|
|
</head>
|
||
|
|
<body>
|
||
|
|
<nav class="navbar">
|
||
|
|
<a href="/">Home</a>
|
||
|
|
<a href="/clients">Clients</a>
|
||
|
|
<a href="/products">Products</a>
|
||
|
|
</nav>
|
||
|
|
|
||
|
|
<main class="container">
|
||
|
|
```
|
||
|
|
|
||
|
|
**Example Footer (`templates/wrappers/site/footer.php`):**
|
||
|
|
```php
|
||
|
|
</main>
|
||
|
|
|
||
|
|
<footer>
|
||
|
|
<p>© <?php echo date('Y'); ?> My Site</p>
|
||
|
|
</footer>
|
||
|
|
|
||
|
|
<!-- Global JS -->
|
||
|
|
<script src="/assets/js/global.js"></script>
|
||
|
|
|
||
|
|
<!-- Page-specific JS -->
|
||
|
|
<?php
|
||
|
|
$controller = strtolower($this->getControllerName());
|
||
|
|
if (file_exists(PUBLIC_PATH . "/assets/js/{$controller}.js")) {
|
||
|
|
echo "<script src='/assets/js/{$controller}.js'></script>";
|
||
|
|
}
|
||
|
|
?>
|
||
|
|
</body>
|
||
|
|
</html>
|
||
|
|
```
|
||
|
|
|
||
|
|
### 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
|
||
|
|
<!-- public/views/clients/show.php -->
|
||
|
|
<div class="container">
|
||
|
|
<?php $this->partial('_partials/breadcrumb', [
|
||
|
|
'Home' => '/',
|
||
|
|
'Clients' => '/clients',
|
||
|
|
$this->data['client']['name'] => ''
|
||
|
|
]); ?>
|
||
|
|
|
||
|
|
<div class="row">
|
||
|
|
<div class="col-md-8">
|
||
|
|
<h1><?php echo htmlspecialchars($this->data['client']['name']); ?></h1>
|
||
|
|
<!-- Main content -->
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="col-md-4">
|
||
|
|
<?php $this->partial('_partials/sidebar'); ?>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
```
|
||
|
|
|
||
|
|
**Example Partial (`public/views/_partials/breadcrumb.php`):**
|
||
|
|
```php
|
||
|
|
<nav aria-label="breadcrumb">
|
||
|
|
<ol class="breadcrumb">
|
||
|
|
<?php
|
||
|
|
$breadcrumb = $this->data['breadcrumb'] ?? [];
|
||
|
|
$lastItem = array_key_last($breadcrumb);
|
||
|
|
|
||
|
|
foreach ($breadcrumb as $label => $url):
|
||
|
|
if ($label === $lastItem):
|
||
|
|
echo '<li class="breadcrumb-item active" aria-current="page">' .
|
||
|
|
htmlspecialchars($label) . '</li>';
|
||
|
|
else:
|
||
|
|
echo '<li class="breadcrumb-item"><a href="' . $url . '">' .
|
||
|
|
htmlspecialchars($label) . '</a></li>';
|
||
|
|
endif;
|
||
|
|
endforeach;
|
||
|
|
?>
|
||
|
|
</ol>
|
||
|
|
</nav>
|
||
|
|
```
|
||
|
|
|
||
|
|
## 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
|
||
|
|
<?php
|
||
|
|
// public/routes.php (or wherever you configure AltoRouter)
|
||
|
|
|
||
|
|
// Named route with parameter
|
||
|
|
$router->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
|
||
|
|
<?php
|
||
|
|
class Users extends Controller
|
||
|
|
{
|
||
|
|
public function show($id)
|
||
|
|
{
|
||
|
|
// Access route parameters
|
||
|
|
$user = Db::getRow("SELECT * FROM users WHERE id = :id", [':id' => $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
|
||
|
|
<!DOCTYPE html>
|
||
|
|
<html>
|
||
|
|
<head>
|
||
|
|
<style>
|
||
|
|
body { font-family: Arial, sans-serif; }
|
||
|
|
.container { max-width: 600px; margin: 0 auto; }
|
||
|
|
.header { background: #007bff; color: white; padding: 20px; }
|
||
|
|
.content { padding: 20px; }
|
||
|
|
.button { background: #007bff; color: white; padding: 10px 20px; text-decoration: none; }
|
||
|
|
</style>
|
||
|
|
</head>
|
||
|
|
<body>
|
||
|
|
<div class="container">
|
||
|
|
<div class="header">
|
||
|
|
<h1>Welcome to %SITE_NAME%</h1>
|
||
|
|
</div>
|
||
|
|
<div class="content">
|
||
|
|
<p>Hi %FNAME%,</p>
|
||
|
|
<p>Thank you for signing up! We're excited to have you on board.</p>
|
||
|
|
<p>Your account email is: <strong>%EMAIL%</strong></p>
|
||
|
|
<p>
|
||
|
|
<a href="%VERIFY_LINK%" class="button">Verify Your Email</a>
|
||
|
|
</p>
|
||
|
|
<p>Thanks,<br>The %SITE_NAME% Team</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</body>
|
||
|
|
</html>
|
||
|
|
```
|
||
|
|
|
||
|
|
**Using Templates in Code:**
|
||
|
|
```php
|
||
|
|
<?php
|
||
|
|
// app/Services/EmailService.php
|
||
|
|
class EmailService
|
||
|
|
{
|
||
|
|
public static function sendWelcomeEmail($email, $firstName)
|
||
|
|
{
|
||
|
|
// Load template
|
||
|
|
$template = file_get_contents(TEMPLATES_PATH . '/emails/welcome.html');
|
||
|
|
|
||
|
|
// Replace placeholders
|
||
|
|
$replacements = [
|
||
|
|
'%FNAME%' => $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
|
||
|
|
<?php
|
||
|
|
class Clients extends Controller
|
||
|
|
{
|
||
|
|
public function __construct() {
|
||
|
|
parent::__construct();
|
||
|
|
// Initialization logic
|
||
|
|
}
|
||
|
|
|
||
|
|
public function index() {
|
||
|
|
// Will be available as $this->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
|
||
|
|
<h1><?php echo $this->data['title']; ?></h1>
|
||
|
|
<?php foreach ($this->data['users'] as $user): ?>
|
||
|
|
<p><?php echo htmlspecialchars($user['name']); ?></p>
|
||
|
|
<?php endforeach; ?>
|
||
|
|
|
||
|
|
// Using partials
|
||
|
|
<?php $this->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
|
||
|
|
<?php
|
||
|
|
class Documents extends Controller
|
||
|
|
{
|
||
|
|
public function upload()
|
||
|
|
{
|
||
|
|
try {
|
||
|
|
if (!isset($_FILES['document'])) {
|
||
|
|
throw new Exception('No file uploaded');
|
||
|
|
}
|
||
|
|
|
||
|
|
$file = $_FILES['document'];
|
||
|
|
|
||
|
|
// Validate file
|
||
|
|
$maxSize = 5 * 1024 * 1024; // 5MB
|
||
|
|
if ($file['size'] > $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
|
||
|
|
<?php
|
||
|
|
class Products extends Controller
|
||
|
|
{
|
||
|
|
public function index()
|
||
|
|
{
|
||
|
|
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
|
||
|
|
$perPage = 20;
|
||
|
|
$offset = ($page - 1) * $perPage;
|
||
|
|
|
||
|
|
// Get total count
|
||
|
|
$total = Db::getRow("SELECT COUNT(*) as count FROM products");
|
||
|
|
$totalPages = ceil($total['count'] / $perPage);
|
||
|
|
|
||
|
|
// Get paginated results
|
||
|
|
$products = Db::select(
|
||
|
|
"SELECT * FROM products ORDER BY created_at DESC LIMIT :limit OFFSET :offset",
|
||
|
|
[':limit' => $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
|
||
|
|
<?php
|
||
|
|
class Clients extends Controller
|
||
|
|
{
|
||
|
|
public function search()
|
||
|
|
{
|
||
|
|
$query = "SELECT * FROM clients WHERE 1=1";
|
||
|
|
$params = [];
|
||
|
|
|
||
|
|
// Search term
|
||
|
|
if (!empty($_GET['q'])) {
|
||
|
|
$query .= " AND (name LIKE :search OR email LIKE :search)";
|
||
|
|
$params[':search'] = '%' . $_GET['q'] . '%';
|
||
|
|
}
|
||
|
|
|
||
|
|
// Status filter
|
||
|
|
if (isset($_GET['status']) && $_GET['status'] !== '') {
|
||
|
|
$query .= " AND status = :status";
|
||
|
|
$params[':status'] = $_GET['status'];
|
||
|
|
}
|
||
|
|
|
||
|
|
// Date range
|
||
|
|
if (!empty($_GET['date_from'])) {
|
||
|
|
$query .= " AND created_at >= :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
|
||
|
|
<?php
|
||
|
|
// app/Helpers/Auth.php
|
||
|
|
class Auth
|
||
|
|
{
|
||
|
|
public static function login($email, $password)
|
||
|
|
{
|
||
|
|
$user = Db::getRow("SELECT * FROM users WHERE email = :email", [':email' => $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
|
||
|
|
<button class="RegularModal btn fw-bold btn-primary" data-url="/modals/load/addorg" data-size="mw-650px">New Company</button>
|
||
|
|
```
|
||
|
|
|
||
|
|
### Building the Modal
|
||
|
|
```html
|
||
|
|
|
||
|
|
<!--begin::Modal dialog-->
|
||
|
|
<!--begin::Modal content-->
|
||
|
|
<div class="modal-content">
|
||
|
|
<!--begin::Modal header-->
|
||
|
|
<div class="modal-header" id="(UNIQUE_ID)">
|
||
|
|
<!--begin::Modal title-->
|
||
|
|
<h2>(Modal Title Name Here)</h2>
|
||
|
|
<!--end::Modal title-->
|
||
|
|
<!--begin::Close-->
|
||
|
|
<div class="btn btn-sm btn-icon btn-active-color-primary" data-bs-dismiss="modal">
|
||
|
|
<i class="ki-duotone ki-cross fs-1">
|
||
|
|
<span class="path1"></span>
|
||
|
|
<span class="path2"></span>
|
||
|
|
</i>
|
||
|
|
</div>
|
||
|
|
<!--end::Close-->
|
||
|
|
</div>
|
||
|
|
<!--end::Modal header-->
|
||
|
|
(MODAL CONTENT HERE)
|
||
|
|
</div>
|
||
|
|
<!--end::Modal content-->
|
||
|
|
<!--end::Modal dialog-->
|
||
|
|
```
|