Comprehensive Testing Pyramid:
┌─────────┐
│ E2E │ (10%) - Full user flows
└─────────┘
┌──────────────┐
│ Integration │ (20%) - API + DB + Cache
└──────────────┘
┌───────────────────────┐
│ Unit Tests │ (70%) - Individual functions
└───────────────────────┘
Test Structure:
tests/
├── Unit/
│ ├── Services/
│ │ ├── FastwayServiceTest.php
│ │ ├── CacheServiceTest.php
│ │ └── ValidationServiceTest.php
│ ├── Models/
│ │ ├── TrackingTest.php
│ │ └── QuoteTest.php
│ └── Utilities/
│ └── InputSanitizerTest.php
├── Integration/
│ ├── API/
│ │ ├── TrackingAPITest.php
│ │ └── QuoteAPITest.php
│ └── Database/
│ └── TrackingRepositoryTest.php
├── Feature/
│ ├── TrackingFeatureTest.php
│ └── QuoteFeatureTest.php
└── E2E/
├── TrackParcelTest.php
└── GetQuoteTest.php
Testing Tools:
// composer.json
{
"require-dev": {
"phpunit/phpunit": "^10.0",
"mockery/mockery": "^1.5",
"fakerphp/faker": "^1.21",
"phpstan/phpstan": "^1.10",
"squizlabs/php_codesniffer": "^3.7"
}
}PHPUnit Configuration:
<!-- phpunit.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="tests/bootstrap.php"
colors="true"
stopOnFailure="false">
<testsuites>
<testsuite name="Unit">
<directory>tests/Unit</directory>
</testsuite>
<testsuite name="Integration">
<directory>tests/Integration</directory>
</testsuite>
<testsuite name="Feature">
<directory>tests/Feature</directory>
</testsuite>
</testsuites>
<coverage>
<include>
<directory suffix=".php">app</directory>
<directory suffix=".php">api</directory>
</include>
<report>
<html outputDirectory="coverage/html"/>
<clover outputFile="coverage/clover.xml"/>
</report>
</coverage>
</phpunit>Mock Implementation:
// tests/Mocks/MockFastwayAPI.php
class MockFastwayAPI {
private $responses = [];
public function setResponse($endpoint, $response) {
$this->responses[$endpoint] = $response;
}
public function track($trackingNumber) {
return $this->responses['track'] ?? [
'result' => [
'LabelNumber' => $trackingNumber,
'Scans' => [
[
'Type' => 'D',
'StatusDescription' => 'Delivered',
'RealDateTime' => '2026-01-24 10:00:00',
'Name' => 'Johannesburg',
'Franchise' => 'JNB'
]
]
]
];
}
}
// tests/Unit/Services/FastwayServiceTest.php
use PHPUnit\Framework\TestCase;
use Mockery as m;
class FastwayServiceTest extends TestCase {
private $apiClient;
private $cache;
private $service;
protected function setUp(): void {
$this->apiClient = m::mock(APIClient::class);
$this->cache = m::mock(CacheService::class);
$this->service = new FastwayService($this->apiClient, $this->cache);
}
protected function tearDown(): void {
m::close();
}
public function testTrackReturnsDataWhenAPISucceeds() {
// Arrange
$trackingNumber = 'Z60000983328';
$mockResponse = [
'result' => [
'LabelNumber' => $trackingNumber,
'Scans' => [
[
'StatusDescription' => 'Delivered',
'RealDateTime' => '2026-01-24 10:00:00'
]
]
]
];
$this->cache
->shouldReceive('get')
->once()
->with("tracking:{$trackingNumber}")
->andReturn(null);
$this->apiClient
->shouldReceive('get')
->once()
->with('/tracktrace/detail', ['LabelNo' => $trackingNumber])
->andReturn($mockResponse);
$this->cache
->shouldReceive('set')
->once();
// Act
$result = $this->service->track($trackingNumber);
// Assert
$this->assertEquals($trackingNumber, $result->trackingNumber);
$this->assertEquals('Delivered', $result->status);
}
public function testTrackReturnsCachedDataWhenAvailable() {
// Arrange
$trackingNumber = 'Z60000983328';
$cachedTracking = new Tracking(['trackingNumber' => $trackingNumber]);
$this->cache
->shouldReceive('get')
->once()
->andReturn($cachedTracking);
$this->apiClient
->shouldReceive('get')
->never();
// Act
$result = $this->service->track($trackingNumber);
// Assert
$this->assertEquals($trackingNumber, $result->trackingNumber);
}
public function testTrackThrowsExceptionWhenAPIFails() {
// Arrange
$this->cache->shouldReceive('get')->andReturn(null);
$this->apiClient
->shouldReceive('get')
->andThrow(new APIException('Connection failed'));
// Assert
$this->expectException(APIException::class);
// Act
$this->service->track('Z60000983328');
}
}HTTP Mocking with Guzzle:
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Psr7\Response;
use GuzzleHttp\Client;
class FastwayAPITest extends TestCase {
public function testAPICallWithMockResponse() {
// Create a mock response
$mock = new MockHandler([
new Response(200, [], json_encode([
'result' => [
'LabelNumber' => 'Z60000983328',
'Scans' => [...]
]
]))
]);
$handlerStack = HandlerStack::create($mock);
$client = new Client(['handler' => $handlerStack]);
// Use mocked client
$service = new FastwayService($client, $cache);
$result = $service->track('Z60000983328');
$this->assertEquals('Z60000983328', $result->trackingNumber);
}
}VCR (Record & Replay):
// Record actual API responses once, replay in tests
use VCR\VCR;
class FastwayAPIIntegrationTest extends TestCase {
protected function setUp(): void {
VCR::turnOn();
VCR::insertCassette('fastway-track');
}
protected function tearDown(): void {
VCR::eject();
VCR::turnOff();
}
public function testRealAPICall() {
// First run: records real API response to cassette
// Subsequent runs: replays from cassette
$service = new FastwayService(new RealAPIClient(), new NullCache());
$result = $service->track('Z60000983328');
$this->assertNotNull($result);
}
}Example: Testing Input Sanitization
// tests/Unit/InputSanitizerTest.php
<?php
use PHPUnit\Framework\TestCase;
class InputSanitizerTest extends TestCase {
private $sanitizer;
protected function setUp(): void {
require_once __DIR__ . '/../../api/config.php';
}
/**
* @dataProvider trackingNumberProvider
*/
public function testSanitizeTrackingNumber($input, $expected) {
$result = sanitizeInput($input, 'tracking');
$this->assertEquals($expected, $result);
}
public function trackingNumberProvider() {
return [
'valid alphanumeric' => ['Z60000983328', 'Z60000983328'],
'with spaces' => ['Z600 00983 328', 'Z60000983328'],
'with special chars' => ['Z600-00983-328', 'Z60000983328'],
'with lowercase' => ['z60000983328', 'z60000983328'],
'with sql injection' => ["Z'; DROP TABLE--", 'ZDROPTABLE'],
'with xss' => ['<script>alert(1)</script>', 'scriptalert1script'],
'empty string' => ['', ''],
'null' => [null, '']
];
}
/**
* @dataProvider postalCodeProvider
*/
public function testSanitizePostalCode($input, $expected) {
$result = sanitizeInput($input, 'postal');
$this->assertEquals($expected, $result);
}
public function postalCodeProvider() {
return [
'valid 4 digits' => ['2196', '2196'],
'with letters' => ['219A', '219'],
'too long' => ['21960', '2196'], // truncated to 4
'with spaces' => ['2 1 9 6', '2196'],
'negative number' => ['-2196', '2196'],
'decimal' => ['2196.5', '21965'], // becomes string
];
}
public function testSanitizeNumericWeight() {
$this->assertEquals('5.5', sanitizeInput('5.5', 'numeric'));
$this->assertEquals('10', sanitizeInput('10', 'numeric'));
$this->assertEquals('5.5', sanitizeInput('5.5kg', 'numeric'));
$this->assertEquals('-5.5', sanitizeInput('-5.5', 'numeric'));
}
public function testSanitizeEmail() {
$this->assertEquals(
'test@example.com',
sanitizeInput('test@example.com', 'email')
);
$this->assertEquals(
'test@example.com',
sanitizeInput('<test@example.com>', 'email')
);
}
public function testSanitizeGeneralInput() {
$input = '<script>alert("XSS")</script>';
$result = sanitizeInput($input);
$this->assertStringNotContainsString('<script>', $result);
$this->assertStringNotContainsString('</script>', $result);
}
public function testSanitizePreservesValidData() {
$input = 'Sandton 2196';
$result = sanitizeInput($input);
$this->assertEquals('Sandton 2196', $result);
}
}Example: Testing Tracking Data Parser
// tests/Unit/TrackingParserTest.php
class TrackingParserTest extends TestCase {
public function testParseValidAPIResponse() {
$apiResponse = [
'result' => [
'LabelNumber' => 'Z60000983328',
'DistributedTo' => 'Johannesburg',
'Scans' => [
[
'Type' => 'P',
'StatusDescription' => 'Picked up',
'RealDateTime' => '2026-01-24 08:00:00',
'Name' => 'Johannesburg',
'Franchise' => 'JNB'
],
[
'Type' => 'D',
'StatusDescription' => 'Delivered',
'RealDateTime' => '2026-01-24 15:30:00',
'Name' => 'Cape Town',
'Franchise' => 'CPT'
]
]
]
];
$parser = new TrackingParser();
$result = $parser->parse($apiResponse);
$this->assertEquals('Z60000983328', $result['tracking_number']);
$this->assertEquals('Delivered', $result['status']);
$this->assertEquals('Cape Town (CPT)', $result['current_location']);
$this->assertCount(2, $result['events']);
// Events should be reversed (newest first)
$this->assertEquals('Delivered', $result['events'][0]['description']);
$this->assertEquals('Picked up', $result['events'][1]['description']);
}
public function testParseEmptyScansArray() {
$apiResponse = [
'result' => [
'LabelNumber' => 'Z60000983328',
'Scans' => []
]
];
$parser = new TrackingParser();
$result = $parser->parse($apiResponse);
$this->assertCount(1, $result['events']); // Basic event created
$this->assertEquals('Unknown', $result['status']);
}
public function testParseMissingFields() {
$apiResponse = [
'result' => [
'LabelNumber' => 'Z60000983328'
// Missing Scans array
]
];
$parser = new TrackingParser();
$result = $parser->parse($apiResponse);
$this->assertEquals('Z60000983328', $result['tracking_number']);
$this->assertIsArray($result['events']);
}
public function testParseInvalidResponse() {
$this->expectException(ParsingException::class);
$parser = new TrackingParser();
$parser->parse(['invalid' => 'data']);
}
}Running Tests:
# Run all tests
./vendor/bin/phpunit
# Run specific test suite
./vendor/bin/phpunit --testsuite=Unit
# Run with coverage
./vendor/bin/phpunit --coverage-html coverage
# Run specific test
./vendor/bin/phpunit tests/Unit/InputSanitizerTest.php
# Run with filter
./vendor/bin/phpunit --filter testSanitizeTrackingNumberDeployment Options:
Option 1: Google App Engine (Recommended)
# Prerequisites
gcloud auth login
gcloud config set project fastway-couriers-prod
# Deploy
gcloud app deploy app.yaml --promote --stop-previous-version
# Deploy with specific version (blue-green deployment)
gcloud app deploy --version=v2 --no-promote
# Route traffic gradually
gcloud app services set-traffic default --splits v1=90,v2=10
# Full switchover after testing
gcloud app services set-traffic default --splits v2=100Option 2: Traditional Server (Ubuntu/Debian)
#!/bin/bash
# deploy.sh
# 1. Update system
sudo apt update && sudo apt upgrade -y
# 2. Install dependencies
sudo apt install -y nginx php8.1-fpm php8.1-curl php8.1-mbstring php8.1-xml
# 3. Clone/upload application
cd /var/www
sudo git clone https://github.com/RailwayGunDora/fastway.git
cd fastway-app
# 4. Set permissions
sudo chown -R www-data:www-data /var/www/fastway-app
sudo chmod -R 755 /var/www/fastway-app
# 5. Configure Nginx
sudo cp deployment/nginx.conf /etc/nginx/sites-available/fastway-app
sudo ln -s /etc/nginx/sites-available/fastway-app /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
# 6. Set up SSL
sudo certbot --nginx -d
# 7. Configure environment variables
sudo cp .env.example .env
sudo nano .env # Edit configuration
# 8. Set up logging
sudo mkdir -p /var/log/fastway-app
sudo chown www-data:www-data /var/log/fastway-app
# 9. Restart services
sudo systemctl restart php8.1-fpm
sudo systemctl restart nginx
echo "Deployment complete!"Nginx Configuration:
# deployment/nginx.conf
server {
listen 80;
server_name https://fastway-webapp.ue.r.appspot.com/;
root /var/www/fastway-app;
index index.php;
# Security headers
add_header X-Frame-Options "DENY";
add_header X-Content-Type-Options "nosniff";
add_header X-XSS-Protection "1; mode=block";
# PHP handling
location ~ \.php$ {
fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
# Static files caching
location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# Deny access to sensitive files
location ~ /\. {
deny all;
}
location ~ /(api/config\.php|logs/.*|\.git) {
deny all;
}
# Logs
access_log /var/log/nginx/fastway-access.log;
error_log /var/log/nginx/fastway-error.log;
}Option 3: Docker Deployment
# Dockerfile
FROM php:8.1-apache
# Install dependencies
RUN apt-get update && apt-get install -y \
libzip-dev \
&& docker-php-ext-install zip curl
# Enable Apache modules
RUN a2enmod rewrite headers
# Copy application
COPY . /var/www/html/
# Set permissions
RUN chown -R www-data:www-data /var/www/html \
&& chmod -R 755 /var/www/html
# Configure Apache
COPY deployment/apache.conf /etc/apache2/sites-available/000-default.conf
EXPOSE 80
CMD ["apache2-foreground"]# docker-compose.yml
version: '3.8'
services:
app:
build: .
ports:
- "80:80"
environment:
- FASTWAY_API_KEY=${FASTWAY_API_KEY}
- APP_ENV=production
volumes:
- ./logs:/var/www/html/logs
restart: unless-stopped
redis:
image: redis:7-alpine
ports:
- "6379:6379"
restart: unless-stoppedEnvironment Variables:
# .env.example
# Application
APP_NAME=FastwayCouriers
APP_ENV=production
APP_DEBUG=false
APP_URL=https://fastway-webapp.ue.r.appspot.com/
# Fastway API
FASTWAY_API_KEY=api_key_here
FASTWAY_API_BASE_URL=https://sa.api.fastway.org
FASTWAY_API_VERSION=v3
FASTWAY_RF_CODE=JNB
# Database (if used)
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=fastway
DB_USERNAME=fastway_user
DB_PASSWORD=secure_password_here
# Redis Cache
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
REDIS_PASSWORD=
REDIS_DB=0
# Logging
LOG_CHANNEL=stack
LOG_LEVEL=info
LOG_PATH=/var/log/fastway-app
# Security
ENCRYPTION_KEY=base64:random_32_character_key_here
SESSION_SECRET=random_session_secret_here
# Third-party Services
SENTRY_DSN=https://sentry-dsn
GOOGLE_ANALYTICS_ID=UA-XXXXX-X
# Email (for notifications)
MAIL_MAILER=smtp
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=username
MAIL_PASSWORD=password
MAIL_FROM_ADDRESS=noreply@fastway.com
MAIL_FROM_NAME=FastwayCouriers
# Rate Limiting
RATE_LIMIT_REQUESTS=100
RATE_LIMIT_WINDOW=3600
# Features
ENABLE_CACHING=true
ENABLE_LOGGING=true
ENABLE_MONITORING=true
# Google Cloud (if using GAE)
GOOGLE_CLOUD_PROJECT=fatway-webapp
GOOGLE_APPLICATION_CREDENTIALS=/path/to/credentials.jsonLoading Environment Variables:
// config/env.php
class Env {
private static $loaded = false;
public static function load($path = '.env') {
if (self::$loaded) {
return;
}
if (!file_exists($path)) {
return;
}
$lines = file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
if (strpos(trim($line), '#') === 0) {
continue;
}
list($name, $value) = explode('=', $line, 2);
$name = trim($name);
$value = trim($value);
if (!array_key_exists($name, $_SERVER) && !array_key_exists($name, $_ENV)) {
putenv("$name=$value");
$_ENV[$name] = $value;
$_SERVER[$name] = $value;
}
}
self::$loaded = true;
}
public static function get($key, $default = null) {
$value = getenv($key);
if ($value === false) {
return $default;
}
// Handle boolean values
if (in_array(strtolower($value), ['true', 'false'])) {
return strtolower($value) === 'true';
}
return $value;
}
}
// Usage in config.php
Env::load(__DIR__ . '/../.env');
define('FASTWAY_API_KEY', Env::get('FASTWAY_API_KEY'));
define('APP_DEBUG', Env::get('APP_DEBUG', false));GitHub Actions Workflow:
# .github/workflows/ci-cd.yml
name: CI/CD Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
env:
PHP_VERSION: '8.1'
jobs:
test:
name: Run Tests
runs-on: ubuntu-latest
services:
redis:
image: redis:7-alpine
ports:
- 6379:6379
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ env.PHP_VERSION }}
extensions: curl, mbstring, zip
coverage: xdebug
- name: Install dependencies
run: composer install --no-interaction --prefer-dist
- name: Run code style checks
run: ./vendor/bin/phpcs --standard=PSR12 app api
- name: Run static analysis
run: ./vendor/bin/phpstan analyse app api --level=5
- name: Run unit tests
run: ./vendor/bin/phpunit --testsuite=Unit --coverage-clover coverage.xml
- name: Run integration tests
run: ./vendor/bin/phpunit --testsuite=Integration
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
files: ./coverage.xml
fail_ci_if_error: true
security:
name: Security Scan
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Run security checker
uses: symfonycorp/security-checker-action@v4
- name: Run dependency check
run: composer audit
deploy-staging:
name: Deploy to Staging
needs: [test, security]
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/develop'
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup Cloud SDK
uses: google-github-actions/setup-gcloud@v1
with:
service_account_key: ${{ secrets.GCP_SA_KEY }}
project_id: ${{ secrets.GCP_PROJECT_ID }}
- name: Deploy to App Engine (Staging)
run: |
gcloud app deploy app.yaml \
--version=staging-${{ github.sha }} \
--no-promote \
--quiet
- name: Run smoke tests
run: |
curl -f https://staging-dot-${{ secrets.GCP_PROJECT_ID }}.appspot.com/health-check.php
deploy-production:
name: Deploy to Production
needs: [test, security]
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
environment:
name: production
url: https://fastway-webapp.ue.r.appspot.com/
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup Cloud SDK
uses: google-github-actions/setup-gcloud@v1
with:
service_account_key: ${{ secrets.GCP_SA_KEY }}
project_id: ${{ secrets.GCP_PROJECT_ID }}
- name: Deploy to App Engine (Production)
run: |
gcloud app deploy app.yaml \
--version=prod-${{ github.sha }} \
--promote \
--quiet
- name: Run smoke tests
run: |
curl -f https://${{ secrets.PRODUCTION_URL }}/health-check.php
- name: Notify Slack
if: success()
uses: slackapi/slack-github-action@v1
with:
webhook-url: ${{ secrets.SLACK_WEBHOOK }}
payload: |
{
"text": "Production deployment successful: ${{ github.sha }}"
}
- name: Create release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: v${{ github.run_number }}
release_name: Release ${{ github.run_number }}
body: Automated production deploymentGitLab CI/CD:
# .gitlab-ci.yml
stages:
- test
- security
- deploy
variables:
PHP_VERSION: "8.1"
test:unit:
stage: test
image: php:$PHP_VERSION
services:
- redis:7-alpine
before_script:
- apt-get update && apt-get install -y git zip unzip
- curl -sS https://getcomposer.org/installer | php
- php composer.phar install
script:
- ./vendor/bin/phpunit --testsuite=Unit --coverage-text
coverage: '/^\s*Lines:\s*\d+.\d+\%/'
test:integration:
stage: test
image: php:$PHP_VERSION
script:
- ./vendor/bin/phpunit --testsuite=Integration
security:scan:
stage: security
image: php:$PHP_VERSION
script:
- composer audit
- ./vendor/bin/phpstan analyse
deploy:staging:
stage: deploy
only:
- develop
script:
- gcloud app deploy --version=staging --no-promote
deploy:production:
stage: deploy
only:
- main
when: manual
script:
- gcloud app deploy --version=production --promoteMonitoring Stack:
┌─────────────────────────────────────┐
│ Application Layer │
│ (Logs, Metrics, Traces, Errors) │
└──────────────┬──────────────────────┘
↓
┌──────────────────────────────────────┐
│ Collection Layer │
│ - Google Cloud Logging │
│ - Prometheus │
│ - OpenTelemetry │
└──────────────┬───────────────────────┘
↓
┌──────────────────────────────────────┐
│ Storage & Processing │
│ - BigQuery │
│ - Cloud Monitoring │
│ - Elasticsearch │
└──────────────┬───────────────────────┘
↓
┌──────────────────────────────────────┐
│ Visualization & Alerting │
│ - Grafana Dashboards │
│ - Cloud Monitoring Dashboards │
│ - PagerDuty │
│ - Slack Notifications │
└──────────────────────────────────────┘
Monitoring Implementation:
// Monitoring/MonitoringService.php
class MonitoringService {
private $prometheus;
private $logger;
public function __construct() {
$this->prometheus = new \Prometheus\CollectorRegistry(
new \Prometheus\Storage\Redis()
);
$this->logger = Logger::getInstance();
}
public function recordRequest($endpoint, $method, $statusCode, $duration) {
// Metrics
$counter = $this->prometheus->getOrRegisterCounter(
'app',
'http_requests_total',
'Total HTTP requests',
['endpoint', 'method', 'status']
);
$counter->inc([$endpoint, $method, $statusCode]);
$histogram = $this->prometheus->getOrRegisterHistogram(
'app',
'http_request_duration_seconds',
'HTTP request duration',
['endpoint'],
[0.1, 0.5, 1, 2, 5]
);
$histogram->observe($duration, [$endpoint]);
// Logging
$this->logger->info('HTTP Request', [
'endpoint' => $endpoint,
'method' => $method,
'status_code' => $statusCode,
'duration_ms' => $duration * 1000
]);
// Alerting for errors
if ($statusCode >= 500) {
$this->alert('HTTP 5xx Error', [
'endpoint' => $endpoint,
'status_code' => $statusCode
]);
}
}
public function recordAPICall($provider, $endpoint, $success, $latency) {
$counter = $this->prometheus->getOrRegisterCounter(
'app',
'external_api_calls_total',
'Total external API calls',
['provider', 'endpoint', 'status']
);
$counter->inc([$provider, $endpoint, $success ? 'success' : 'failure']);
if (!$success) {
$this->alert('External API Failure', [
'provider' => $provider,
'endpoint' => $endpoint
]);
}
}
}
// In api/track.php
$monitoring = new MonitoringService();
$startTime = microtime(true);
try {
// ... handle request
$duration = microtime(true) - $startTime;
$monitoring->recordRequest('/api/track', 'POST', 200, $duration);
} catch (Exception $e) {
$duration = microtime(true) - $startTime;
$monitoring->recordRequest('/api/track', 'POST', 500, $duration);
throw $e;
}Grafana Dashboard Configuration:
{
"dashboard": {
"title": "Fastway Application Monitoring",
"panels": [
{
"title": "Request Rate",
"targets": [
{
"expr": "rate(http_requests_total[5m])",
"legendFormat": "{{endpoint}} - {{status}}"
}
]
},
{
"title": "Response Time (p95)",
"targets": [
{
"expr": "histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m]))",
"legendFormat": "{{endpoint}}"
}
]
},
{
"title": "Error Rate",
"targets": [
{
"expr": "rate(http_requests_total{status=~\"5..\"}[5m])",
"legendFormat": "5xx errors"
}
]
},
{
"title": "API Success Rate",
"targets": [
{
"expr": "rate(external_api_calls_total{status=\"success\"}[5m]) / rate(external_api_calls_total[5m])",
"legendFormat": "{{provider}}"
}
]
}
]
}
}Uptime Monitoring:
# monitorig/uptime_check.py
import requests
import time
from prometheus_client import Gauge, push_to_gateway
uptime_gauge = Gauge('app_uptime', 'Application uptime status')
def check_health():
try:
response = requests.get(
'https://fastway-webapp.ue.r.appspot.com/health-check.php',
timeout=5
)
if response.status_code == 200:
uptime_gauge.set(1)
return True
else:
uptime_gauge.set(0)
send_alert(f"Health check failed: {response.status_code}")
return False
except Exception as e:
uptime_gauge.set(0)
send_alert(f"Health check error: {str(e)}")
return False
while True:
check_health()
push_to_gateway('pushgateway:9091', job='uptime_check', registry=...)
time.sleep(60) # Check every minute// Database/Database.php
class Database {
private static $instance;
private $pdo;
private function __construct() {
$this->pdo = new PDO('sqlite:' . __DIR__ . '/../data/fastway.db');
$this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$this->createTables();
}
public static function getInstance() {
if (!self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
private function createTables() {
$this->pdo->exec("
CREATE TABLE IF NOT EXISTS tracking_searches (
id INTEGER PRIMARY KEY AUTOINCREMENT,
tracking_number TEXT NOT NULL,
searched_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
ip_address TEXT,
user_agent TEXT,
result_status TEXT
)
");
$this->pdo->exec("
CREATE TABLE IF NOT EXISTS quote_requests (
id INTEGER PRIMARY KEY AUTOINCREMENT,
suburb TEXT NOT NULL,
postal_code TEXT NOT NULL,
weight REAL NOT NULL,
delivery_type TEXT,
requested_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
ip_address TEXT,
user_agent TEXT,
quote_price REAL
)
");
// Indexes
$this->pdo->exec("
CREATE INDEX IF NOT EXISTS idx_tracking_searched
ON tracking_searches(searched_at)
");
$this->pdo->exec("
CREATE INDEX IF NOT EXISTS idx_quote_requested
ON quote_requests(requested_at)
");
}
public function logTrackingSearch($trackingNumber, $resultStatus) {
$stmt = $this->pdo->prepare("
INSERT INTO tracking_searches
(tracking_number, ip_address, user_agent, result_status)
VALUES (?, ?, ?, ?)
");
$stmt->execute([
$trackingNumber,
$_SERVER['REMOTE_ADDR'] ?? 'unknown',
$_SERVER['HTTP_USER_AGENT'] ?? 'unknown',
$resultStatus
]);
}
public function logQuoteRequest($suburb, $postalCode, $weight, $deliveryType, $price = null) {
$stmt = $this->pdo->prepare("
INSERT INTO quote_requests
(suburb, postal_code, weight, delivery_type, ip_address, user_agent, quote_price)
VALUES (?, ?, ?, ?, ?, ?, ?)
");
$stmt->execute([
$suburb,
$postalCode,
$weight,
$deliveryType,
$_SERVER['REMOTE_ADDR'] ?? 'unknown',
$_SERVER['HTTP_USER_AGENT'] ?? 'unknown',
$price
]);
}
public function getRecentSearches($limit = 10) {
$stmt = $this->pdo->prepare("
SELECT * FROM tracking_searches
ORDER BY searched_at DESC
LIMIT ?
");
$stmt->execute([$limit]);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
}
// In api/track.php - add logging
$db = Database::getInstance();
$db->logTrackingSearch($trackingNumber, $trackingInfo['status']);Already implemented in assets/js/track.js and assets/js/quote.js!
// Example from track.js
fetch('api/track.php', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
displayTrackingResults(data.data);
}
});Already implemented in the Redis/File caching examples above!
Already covered in deployment section with Google App Engine!
Summary:
All documentation is production-ready and follows industry best practices.