Backend Architecture - PHP Phalcon
Overview
This document outlines the backend architecture, API design, and best practices for our SaaS CRM application built with PHP Phalcon framework.
Technology Stack
Core Technologies
| Technology | Version | Purpose |
|---|---|---|
| PHP | 8.2+ | Programming language |
| Phalcon | 5.x | High-performance PHP framework |
| MySQL | 8.0+ | Primary database |
| Redis | 7.x | Caching and sessions |
| Elasticsearch | 8.x | Full-text search |
| RabbitMQ | 3.x | Message queue |
Additional Libraries
{
"require": {
"php": ">=8.2",
"ext-phalcon": "~5.0",
"firebase/php-jwt": "^6.0",
"guzzlehttp/guzzle": "^7.0",
"monolog/monolog": "^3.0",
"phpmailer/phpmailer": "^6.0",
"predis/predis": "^2.0",
"elasticsearch/elasticsearch": "^8.0"
}
}
Project Structure
Directory Layout
phalcon-app/
├── app/
│ ├── config/ # Configuration files
│ │ ├── config.php
│ │ ├── loader.php
│ │ ├── routes.php
│ │ └── services.php
│ ├── controllers/ # API controllers
│ │ ├── BaseController.php
│ │ ├── AuthController.php
│ │ └── CustomerController.php
│ ├── models/ # Database models
│ │ ├── Users.php
│ │ ├── Customers.php
│ │ └── Orders.php
│ ├── services/ # Business logic
│ │ ├── AuthService.php
│ │ ├── CustomerService.php
│ │ └── EmailService.php
│ ├── repositories/ # Data access layer
│ │ ├── UserRepository.php
│ │ └── CustomerRepository.php
│ ├── validators/ # Input validation
│ │ ├── AuthValidator.php
│ │ └── CustomerValidator.php
│ ├── middleware/ # Request middleware
│ │ ├── AuthMiddleware.php
│ │ ├── CorsMiddleware.php
│ │ └── RateLimitMiddleware.php
│ ├── library/ # Custom libraries
│ │ ├── JWT.php
│ │ └── Helpers.php
│ ├── migrations/ # Database migrations
│ └── tasks/ # CLI tasks
├── public/
│ └── index.php # Entry point
├── storage/
│ ├── cache/ # File cache
│ ├── logs/ # Application logs
│ └── uploads/ # User uploads
├── tests/
│ ├── unit/
│ └── integration/
├── vendor/ # Composer dependencies
└── composer.json # PHP dependencies
MVC Architecture
Controller Implementation
<?php
// app/controllers/CustomerController.php
namespace App\Controllers;
use Phalcon\Mvc\Controller;
use Phalcon\Http\Response;
use App\Services\CustomerService;
use App\Validators\CustomerValidator;
use App\Models\Customers;
class CustomerController extends BaseController
{
private CustomerService $customerService;
private CustomerValidator $validator;
public function initialize()
{
parent::initialize();
$this->customerService = new CustomerService();
$this->validator = new CustomerValidator();
}
/**
* GET /api/customers
* List all customers with pagination
*/
public function indexAction()
{
try {
$page = $this->request->getQuery('page', 'int', 1);
$limit = $this->request->getQuery('limit', 'int', 20);
$search = $this->request->getQuery('search', 'string', '');
$sort = $this->request->getQuery('sort', 'string', 'created_at');
$order = $this->request->getQuery('order', 'string', 'desc');
$result = $this->customerService->getCustomers([
'page' => $page,
'limit' => $limit,
'search' => $search,
'sort' => $sort,
'order' => $order
]);
return $this->successResponse($result);
} catch (\Exception $e) {
return $this->errorResponse($e->getMessage(), 500);
}
}
/**
* GET /api/customers/{id}
* Get single customer
*/
public function showAction($id)
{
try {
$customer = $this->customerService->getCustomerById($id);
if (!$customer) {
return $this->errorResponse('Customer not found', 404);
}
return $this->successResponse($customer);
} catch (\Exception $e) {
return $this->errorResponse($e->getMessage(), 500);
}
}
/**
* POST /api/customers
* Create new customer
*/
public function createAction()
{
try {
$data = $this->request->getJsonRawBody(true);
// Validate input
$validation = $this->validator->validateCreate($data);
if ($validation->failed()) {
return $this->errorResponse($validation->getErrors(), 422);
}
$customer = $this->customerService->createCustomer($data);
return $this->successResponse($customer, 201);
} catch (\Exception $e) {
return $this->errorResponse($e->getMessage(), 500);
}
}
/**
* PUT /api/customers/{id}
* Update customer
*/
public function updateAction($id)
{
try {
$data = $this->request->getJsonRawBody(true);
// Validate input
$validation = $this->validator->validateUpdate($data);
if ($validation->failed()) {
return $this->errorResponse($validation->getErrors(), 422);
}
$customer = $this->customerService->updateCustomer($id, $data);
if (!$customer) {
return $this->errorResponse('Customer not found', 404);
}
return $this->successResponse($customer);
} catch (\Exception $e) {
return $this->errorResponse($e->getMessage(), 500);
}
}
/**
* DELETE /api/customers/{id}
* Delete customer
*/
public function deleteAction($id)
{
try {
$result = $this->customerService->deleteCustomer($id);
if (!$result) {
return $this->errorResponse('Customer not found', 404);
}
return $this->successResponse(['message' => 'Customer deleted successfully']);
} catch (\Exception $e) {
return $this->errorResponse($e->getMessage(), 500);
}
}
}
Model Implementation
<?php
// app/models/Customers.php
namespace App\Models;
use Phalcon\Mvc\Model;
use Phalcon\Mvc\Model\Behavior\Timestampable;
use Phalcon\Validation;
use Phalcon\Validation\Validator\Email;
use Phalcon\Validation\Validator\Uniqueness;
class Customers extends Model
{
public $id;
public $name;
public $email;
public $phone;
public $company;
public $status;
public $created_at;
public $updated_at;
public $deleted_at;
public function initialize()
{
$this->setSource('customers');
// Relationships
$this->hasMany('id', Orders::class, 'customer_id', [
'alias' => 'orders'
]);
$this->hasMany('id', Notes::class, 'customer_id', [
'alias' => 'notes'
]);
// Behaviors
$this->addBehavior(
new Timestampable([
'beforeCreate' => [
'field' => 'created_at',
'format' => 'Y-m-d H:i:s'
],
'beforeUpdate' => [
'field' => 'updated_at',
'format' => 'Y-m-d H:i:s'
]
])
);
}
public function validation()
{
$validator = new Validation();
$validator->add('email', new Email([
'message' => 'Invalid email format'
]));
$validator->add('email', new Uniqueness([
'message' => 'Email already exists'
]));
return $this->validate($validator);
}
// Soft delete
public function beforeDelete()
{
$this->deleted_at = date('Y-m-d H:i:s');
$this->save();
return false; // Prevent actual deletion
}
// Scopes
public static function active()
{
return self::find([
'conditions' => 'deleted_at IS NULL AND status = :status:',
'bind' => ['status' => 'active']
]);
}
// Accessors
public function getFullInfo()
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
'company' => $this->company,
'orders_count' => $this->orders->count(),
'total_revenue' => $this->calculateTotalRevenue()
];
}
private function calculateTotalRevenue()
{
return $this->orders->sum(function($order) {
return $order->total;
});
}
}
Service Layer
<?php
// app/services/CustomerService.php
namespace App\Services;
use App\Models\Customers;
use App\Repositories\CustomerRepository;
use Phalcon\Di\Injectable;
use Phalcon\Cache\Cache;
class CustomerService extends Injectable
{
private CustomerRepository $repository;
private Cache $cache;
public function __construct()
{
$this->repository = new CustomerRepository();
$this->cache = $this->getDI()->get('cache');
}
public function getCustomers(array $params)
{
$cacheKey = 'customers:' . md5(json_encode($params));
// Check cache
$cached = $this->cache->get($cacheKey);
if ($cached !== null) {
return $cached;
}
// Build query
$builder = $this->modelsManager->createBuilder()
->from(Customers::class)
->where('deleted_at IS NULL');
// Search
if (!empty($params['search'])) {
$builder->andWhere(
'name LIKE :search: OR email LIKE :search: OR company LIKE :search:',
['search' => '%' . $params['search'] . '%']
);
}
// Sorting
$builder->orderBy($params['sort'] . ' ' . $params['order']);
// Pagination
$paginator = new \Phalcon\Paginator\Adapter\QueryBuilder([
'builder' => $builder,
'limit' => $params['limit'],
'page' => $params['page']
]);
$page = $paginator->paginate();
$result = [
'data' => $page->items->toArray(),
'meta' => [
'total' => $page->total_items,
'per_page' => $page->limit,
'current_page' => $page->current,
'last_page' => $page->last,
'from' => ($page->current - 1) * $page->limit + 1,
'to' => min($page->current * $page->limit, $page->total_items)
]
];
// Cache for 5 minutes
$this->cache->set($cacheKey, $result, 300);
return $result;
}
public function createCustomer(array $data)
{
$customer = new Customers();
$customer->assign($data);
if (!$customer->save()) {
throw new \Exception(implode(', ', $customer->getMessages()));
}
// Clear cache
$this->clearCustomerCache();
// Send welcome email
$this->eventsManager->fire('customer:created', $customer);
return $customer->toArray();
}
public function updateCustomer($id, array $data)
{
$customer = Customers::findFirst($id);
if (!$customer) {
return null;
}
$customer->assign($data);
if (!$customer->save()) {
throw new \Exception(implode(', ', $customer->getMessages()));
}
// Clear cache
$this->clearCustomerCache();
$this->cache->delete('customer:' . $id);
return $customer->toArray();
}
private function clearCustomerCache()
{
$keys = $this->cache->queryKeys('customers:*');
foreach ($keys as $key) {
$this->cache->delete($key);
}
}
}
API Design
RESTful Endpoints
# Customer endpoints
GET /api/customers # List customers
GET /api/customers/{id} # Get customer
POST /api/customers # Create customer
PUT /api/customers/{id} # Update customer
DELETE /api/customers/{id} # Delete customer
# Nested resources
GET /api/customers/{id}/orders # Customer orders
GET /api/customers/{id}/notes # Customer notes
POST /api/customers/{id}/notes # Add note
# Bulk operations
POST /api/customers/bulk-import # Import customers
POST /api/customers/bulk-delete # Delete multiple
POST /api/customers/bulk-update # Update multiple
# Actions
POST /api/customers/{id}/activate # Activate customer
POST /api/customers/{id}/deactivate # Deactivate customer
POST /api/customers/{id}/merge # Merge customers
Request/Response Format
Request Example
{
"name": "John Doe",
"email": "john@example.com",
"phone": "+1234567890",
"company": "Acme Corp",
"address": {
"street": "123 Main St",
"city": "New York",
"state": "NY",
"zip": "10001"
},
"tags": ["vip", "enterprise"]
}
Success Response
{
"success": true,
"data": {
"id": 123,
"name": "John Doe",
"email": "john@example.com",
"created_at": "2025-01-15T10:00:00Z"
},
"meta": {
"timestamp": "2025-01-15T10:00:00Z",
"version": "v1"
}
}
Error Response
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Validation failed",
"details": {
"email": ["Email already exists"],
"phone": ["Invalid phone format"]
}
},
"meta": {
"timestamp": "2025-01-15T10:00:00Z",
"request_id": "req_123456"
}
}
Authentication & Authorization
JWT Implementation
<?php
// app/services/AuthService.php
namespace App\Services;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
use App\Models\Users;
class AuthService
{
private string $secretKey;
private string $algorithm = 'HS256';
private int $tokenLifetime = 3600; // 1 hour
public function __construct()
{
$this->secretKey = $_ENV['JWT_SECRET'];
}
public function login(string $email, string $password)
{
$user = Users::findFirst([
'conditions' => 'email = :email:',
'bind' => ['email' => $email]
]);
if (!$user || !password_verify($password, $user->password)) {
throw new \Exception('Invalid credentials');
}
$token = $this->generateToken($user);
$refreshToken = $this->generateRefreshToken($user);
// Store refresh token
$user->refresh_token = $refreshToken;
$user->save();
return [
'access_token' => $token,
'refresh_token' => $refreshToken,
'expires_in' => $this->tokenLifetime,
'user' => $user->toArray()
];
}
private function generateToken(Users $user)
{
$payload = [
'iss' => $_ENV['APP_URL'],
'sub' => $user->id,
'email' => $user->email,
'role' => $user->role,
'permissions' => $user->getPermissions(),
'iat' => time(),
'exp' => time() + $this->tokenLifetime
];
return JWT::encode($payload, $this->secretKey, $this->algorithm);
}
public function validateToken(string $token)
{
try {
$decoded = JWT::decode($token, new Key($this->secretKey, $this->algorithm));
return (array) $decoded;
} catch (\Exception $e) {
throw new \Exception('Invalid token');
}
}
}
Middleware Implementation
<?php
// app/middleware/AuthMiddleware.php
namespace App\Middleware;
use Phalcon\Mvc\Micro\MiddlewareInterface;
use Phalcon\Mvc\Micro;
use App\Services\AuthService;
class AuthMiddleware implements MiddlewareInterface
{
private AuthService $authService;
public function __construct()
{
$this->authService = new AuthService();
}
public function call(Micro $app)
{
$request = $app->request;
$response = $app->response;
// Skip auth for public routes
$publicRoutes = ['/api/auth/login', '/api/auth/register', '/api/health'];
if (in_array($request->getURI(), $publicRoutes)) {
return true;
}
// Get token from header
$authHeader = $request->getHeader('Authorization');
if (!$authHeader) {
$response->setStatusCode(401);
$response->setJsonContent([
'success' => false,
'error' => 'No authorization token provided'
]);
return false;
}
// Validate token
try {
$token = str_replace('Bearer ', '', $authHeader);
$payload = $this->authService->validateToken($token);
// Store user data for later use
$app->user = $payload;
return true;
} catch (\Exception $e) {
$response->setStatusCode(401);
$response->setJsonContent([
'success' => false,
'error' => $e->getMessage()
]);
return false;
}
}
}
Database Design
Migration System
<?php
// app/migrations/1.0.0/customers.php
use Phalcon\Db\Column;
use Phalcon\Db\Index;
use Phalcon\Db\Reference;
use Phalcon\Migrations\Mvc\Model\Migration;
class CustomersMigration_100 extends Migration
{
public function morph()
{
$this->morphTable('customers', [
'columns' => [
new Column('id', [
'type' => Column::TYPE_INTEGER,
'unsigned' => true,
'notNull' => true,
'autoIncrement' => true,
'size' => 11,
'first' => true
]),
new Column('name', [
'type' => Column::TYPE_VARCHAR,
'notNull' => true,
'size' => 255
]),
new Column('email', [
'type' => Column::TYPE_VARCHAR,
'notNull' => true,
'size' => 255
]),
new Column('phone', [
'type' => Column::TYPE_VARCHAR,
'size' => 20
]),
new Column('company', [
'type' => Column::TYPE_VARCHAR,
'size' => 255
]),
new Column('status', [
'type' => Column::TYPE_ENUM,
'values' => ['active', 'inactive', 'pending'],
'default' => 'active'
]),
new Column('created_at', [
'type' => Column::TYPE_DATETIME,
'notNull' => true
]),
new Column('updated_at', [
'type' => Column::TYPE_DATETIME
]),
new Column('deleted_at', [
'type' => Column::TYPE_DATETIME
])
],
'indexes' => [
new Index('PRIMARY', ['id'], 'PRIMARY'),
new Index('idx_email', ['email'], 'UNIQUE'),
new Index('idx_status', ['status']),
new Index('idx_deleted', ['deleted_at'])
]
]);
}
}
Performance Optimization
Caching Strategy
<?php
// app/config/cache.php
return [
'redis' => [
'adapter' => 'Redis',
'options' => [
'host' => $_ENV['REDIS_HOST'],
'port' => $_ENV['REDIS_PORT'],
'auth' => $_ENV['REDIS_PASSWORD'],
'persistent' => false,
'index' => 0,
'prefix' => 'crm_',
'lifetime' => 3600
]
],
'strategies' => [
'customers' => 300, // 5 minutes
'users' => 600, // 10 minutes
'permissions' => 3600, // 1 hour
'config' => 86400 // 24 hours
]
];
Query Optimization
// Use query builder for complex queries
$customers = $this->modelsManager->createBuilder()
->columns([
'c.id',
'c.name',
'c.email',
'COUNT(o.id) as order_count',
'SUM(o.total) as total_revenue'
])
->from(['c' => Customers::class])
->leftJoin(Orders::class, 'o.customer_id = c.id', 'o')
->where('c.deleted_at IS NULL')
->groupBy('c.id')
->having('order_count > :min:', ['min' => 5])
->orderBy('total_revenue DESC')
->limit(100)
->getQuery()
->execute();
Error Handling
Global Error Handler
<?php
// app/config/services.php
$di->setShared('errorHandler', function() {
return new \App\Library\ErrorHandler();
});
// app/library/ErrorHandler.php
class ErrorHandler
{
public function handle(\Throwable $exception)
{
$logger = Di::getDefault()->get('logger');
// Log error
$logger->error($exception->getMessage(), [
'file' => $exception->getFile(),
'line' => $exception->getLine(),
'trace' => $exception->getTraceAsString()
]);
// Determine response
$code = $exception->getCode() ?: 500;
$message = $_ENV['APP_DEBUG'] ? $exception->getMessage() : 'Internal Server Error';
return [
'success' => false,
'error' => [
'code' => $code,
'message' => $message,
'request_id' => uniqid('req_')
]
];
}
}
Testing
Unit Test Example
<?php
// tests/unit/CustomerServiceTest.php
use PHPUnit\Framework\TestCase;
use App\Services\CustomerService;
use App\Models\Customers;
class CustomerServiceTest extends TestCase
{
private CustomerService $service;
protected function setUp(): void
{
$this->service = new CustomerService();
}
public function testCreateCustomer()
{
$data = [
'name' => 'Test Customer',
'email' => 'test@example.com',
'phone' => '1234567890'
];
$customer = $this->service->createCustomer($data);
$this->assertIsArray($customer);
$this->assertEquals('Test Customer', $customer['name']);
$this->assertEquals('test@example.com', $customer['email']);
}
public function testCreateCustomerWithInvalidEmail()
{
$this->expectException(\Exception::class);
$data = [
'name' => 'Test Customer',
'email' => 'invalid-email'
];
$this->service->createCustomer($data);
}
}
Last Updated: January 2025 Version: 1.0.0