Architecture Overview
Design Pattern
Platform implements Model-View-Controller (MVC) architecture with service layer separation. Each component has specific responsibility for maintainability and scalability.
Core Components
- Models: Data representation and database interaction
- Views: Presentation layer and user interface
- Controllers: Request handling and business logic coordination
- Services: Reusable business logic implementation
- Middleware: Request/response filtering and modification
Request Lifecycle
- HTTP request received by web server
- Request routed to appropriate controller
- Middleware stack processes request
- Controller invokes service methods
- Service interacts with models
- Data retrieved from database
- Response formatted and returned
- View renders final output
Development Environment
Local Setup
Prerequisites
- PHP 8.1 or higher
- Composer 2.0+
- MySQL 8.0+ or PostgreSQL 13+
- Node.js 16+ with npm
- Git version control
- Code editor (VS Code recommended)
Installation Steps
# Clone repository
git clone https://github.com/yourcompany/casino-platform.git
cd casino-platform
Install PHP dependencies
composer install
Install Node dependencies
npm install
Copy environment configuration
cp .env.example .env
Generate application key
php artisan key:generate
Run database migrations
php artisan migrate
Seed initial data
php artisan db:seed
Compile frontend assets
npm run dev
Start development server
php artisan serve
Environment Configuration
.env File Structure
APP_NAME=Casino Platform
APP_ENV=local
APP_DEBUG=true
APP_URL=http://localhost:8000
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=casino_db
DB_USERNAME=root
DB_PASSWORD=
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_MAILER=smtp
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
CACHE_DRIVER=redis
SESSION_DRIVER=redis
QUEUE_CONNECTION=redis
Directory Structure
Application Core
/app
├── Controllers/
│ ├── API/ # API endpoint controllers
│ ├── Auth/ # Authentication controllers
│ ├── Admin/ # Admin panel controllers
│ └── Player/ # Player interface controllers
├── Models/
│ ├── User.php # User model
│ ├── Game.php # Game model
│ ├── Transaction.php # Transaction model
│ └── Bonus.php # Bonus model
├── Services/
│ ├── GameService.php # Game business logic
│ ├── PaymentService.php # Payment processing
│ ├── BonusService.php # Bonus management
│ └── ReportService.php # Reporting logic
├── Middleware/
│ ├── Authenticate.php # Authentication check
│ ├── CheckBalance.php # Balance validation
│ └── RateLimiter.php # Rate limiting
├── Validators/
│ ├── DepositValidator.php
│ ├── WithdrawalValidator.php
│ └── RegistrationValidator.php
└── Helpers/
├── CurrencyHelper.php
├── DateHelper.php
└── FormatHelper.php
Creating Custom Modules
Module Structure
Step 1: Create Controller
<?php
namespace App\Controllers;
class LoyaltyController extends BaseController
{
protected $loyaltyService;
public function __construct(LoyaltyService $service)
{
$this->loyaltyService = $service;
}
public function index()
{
$points = $this->loyaltyService->getUserPoints(
$this->getCurrentUser()->id
);
return $this->view('loyalty.index', [
'points' => $points
]);
}
public function redeem(Request $request)
{
$validator = new RedeemValidator($request->all());
if (!$validator->validate()) {
return $this->error($validator->errors(), 422);
}
$result = $this->loyaltyService->redeemPoints(
$this->getCurrentUser()->id,
$request->input('points')
);
if ($result) {
return $this->success('Points redeemed successfully');
}
return $this->error('Insufficient points', 400);
}
}
Step 2: Create Service
<?php
namespace App\Services;
class LoyaltyService
{
protected $pointsModel;
protected $transactionService;
public function __construct(
LoyaltyPoints $pointsModel,
TransactionService $transactionService
) {
$this->pointsModel = $pointsModel;
$this->transactionService = $transactionService;
}
public function getUserPoints(int $userId): int
{
$cache_key = "loyalty:points:{$userId}";
return Cache::remember($cache_key, 300, function() use ($userId) {
return $this->pointsModel->where('user_id', $userId)
->sum('points');
});
}
public function redeemPoints(int $userId, int $points): bool
{
$available = $this->getUserPoints($userId);
if ($available < $points) {
return false;
}
$amount = $this->calculateRedemptionValue($points);
DB::beginTransaction();
try {
// Deduct points
$this->pointsModel->create([
'user_id' => $userId,
'points' => -$points,
'type' => 'redemption',
'description' => "Redeemed {$points} points"
]);
// Credit account
$this->transactionService->credit($userId, $amount, [
'type' => 'loyalty_redemption',
'reference' => "LP_{$points}"
]);
DB::commit();
Cache::forget("loyalty:points:{$userId}");
return true;
} catch (\Exception $e) {
DB::rollBack();
Log::error('Loyalty redemption failed', [
'user_id' => $userId,
'points' => $points,
'error' => $e->getMessage()
]);
return false;
}
}
private function calculateRedemptionValue(int $points): float
{
return $points * 0.01; // 100 points = $1
}
}
Step 3: Create Model
<?php
namespace App\Models;
class LoyaltyPoints extends Model
{
protected $table = 'loyalty_points';
protected $fillable = [
'user_id',
'points',
'type',
'description'
];
protected $casts = [
'points' => 'integer',
'created_at' => 'datetime'
];
public function user()
{
return $this->belongsTo(User::class);
}
public static function awardPoints(int $userId, int $points, string $reason)
{
return self::create([
'user_id' => $userId,
'points' => $points,
'type' => 'earned',
'description' => $reason
]);
}
}
Step 4: Register Routes
<?php
// routes/web.php
Route::middleware(['auth'])->group(function() {
Route::get('/loyalty', [LoyaltyController::class, 'index'])
->name('loyalty.index');
Route::post('/loyalty/redeem', [LoyaltyController::class, 'redeem'])
->name('loyalty.redeem');
});
Step 5: Create Migration
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
class CreateLoyaltyPointsTable extends Migration
{
public function up()
{
Schema::create('loyalty_points', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->integer('points');
$table->enum('type', ['earned', 'redemption', 'expired']);
$table->text('description')->nullable();
$table->timestamps();
$table->index('user_id');
$table->index('type');
$table->index('created_at');
});
}
public function down()
{
Schema::dropIfExists('loyalty_points');
}
}
Payment Gateway Integration
Creating Payment Processor
Step 1: Create Adapter Interface
<?php
namespace App\Services\Payments;
interface PaymentProcessorInterface
{
public function createDeposit(array $data): array;
public function processWithdrawal(array $data): array;
public function handleCallback(array $data): bool;
public function getTransactionStatus(string $transactionId): string;
public function refundTransaction(string $transactionId): bool;
}
Step 2: Implement Processor
<?php
namespace App\Services\Payments\Processors;
use App\Services\Payments\PaymentProcessorInterface;
class StripeProcessor implements PaymentProcessorInterface
{
protected $apiKey;
protected $client;
public function __construct()
{
$this->apiKey = config('payments.stripe.secret_key');
$this->client = new \Stripe\StripeClient($this->apiKey);
}
public function createDeposit(array $data): array
{
try {
$paymentIntent = $this->client->paymentIntents->create([
'amount' => $data['amount'] * 100, // Convert to cents
'currency' => strtolower($data['currency']),
'customer' => $this->getOrCreateCustomer($data['user_id']),
'metadata' => [
'user_id' => $data['user_id'],
'transaction_id' => $data['transaction_id']
],
'return_url' => $data['return_url']
]);
return [
'status' => 'success',
'payment_id' => $paymentIntent->id,
'client_secret' => $paymentIntent->client_secret,
'redirect_url' => null
];
} catch (\Stripe\Exception\ApiErrorException $e) {
Log::error('Stripe deposit failed', [
'error' => $e->getMessage(),
'data' => $data
]);
return [
'status' => 'error',
'message' => $e->getMessage()
];
}
}
public function processWithdrawal(array $data): array
{
try {
$payout = $this->client->payouts->create([
'amount' => $data['amount'] * 100,
'currency' => strtolower($data['currency']),
'destination' => $data['bank_account_id'],
'metadata' => [
'user_id' => $data['user_id'],
'transaction_id' => $data['transaction_id']
]
]);
return [
'status' => 'success',
'payout_id' => $payout->id,
'estimated_arrival' => $payout->arrival_date
];
} catch (\Stripe\Exception\ApiErrorException $e) {
Log::error('Stripe withdrawal failed', [
'error' => $e->getMessage(),
'data' => $data
]);
return [
'status' => 'error',
'message' => $e->getMessage()
];
}
}
public function handleCallback(array $data): bool
{
$signature = $_SERVER['HTTP_STRIPE_SIGNATURE'] ?? '';
$webhookSecret = config('payments.stripe.webhook_secret');
try {
$event = \Stripe\Webhook::constructEvent(
$data['payload'],
$signature,
$webhookSecret
);
switch ($event->type) {
case 'payment_intent.succeeded':
return $this->handleDepositSuccess($event->data->object);
case 'payment_intent.payment_failed':
return $this->handleDepositFailed($event->data->object);
case 'payout.paid':
return $this->handleWithdrawalSuccess($event->data->object);
default:
Log::info('Unhandled Stripe webhook', ['type' => $event->type]);
return true;
}
} catch (\Exception $e) {
Log::error('Stripe webhook processing failed', [
'error' => $e->getMessage()
]);
return false;
}
}
public function getTransactionStatus(string $transactionId): string
{
try {
$paymentIntent = $this->client->paymentIntents->retrieve($transactionId);
return match($paymentIntent->status) {
'succeeded' => 'completed',
'processing' => 'pending',
'requires_payment_method' => 'pending',
'requires_confirmation' => 'pending',
'canceled' => 'cancelled',
default => 'failed'
};
} catch (\Exception $e) {
return 'unknown';
}
}
public function refundTransaction(string $transactionId): bool
{
try {
$refund = $this->client->refunds->create([
'payment_intent' => $transactionId
]);
return $refund->status === 'succeeded';
} catch (\Exception $e) {
Log::error('Stripe refund failed', [
'transaction_id' => $transactionId,
'error' => $e->getMessage()
]);
return false;
}
}
private function getOrCreateCustomer(int $userId): string
{
$user = User::find($userId);
if ($user->stripe_customer_id) {
return $user->stripe_customer_id;
}
$customer = $this->client->customers->create([
'email' => $user->email,
'name' => $user->first_name . ' ' . $user->last_name,
'metadata' => ['user_id' => $userId]
]);
$user->update(['stripe_customer_id' => $customer->id]);
return $customer->id;
}
private function handleDepositSuccess($paymentIntent): bool
{
$transactionId = $paymentIntent->metadata->transaction_id;
$transaction = Transaction::find($transactionId);
if (!$transaction) {
return false;
}
return app(TransactionService::class)->completeDeposit($transactionId);
}
private function handleDepositFailed($paymentIntent): bool
{
$transactionId = $paymentIntent->metadata->transaction_id;
return app(TransactionService::class)->failTransaction(
$transactionId,
$paymentIntent->last_payment_error->message ?? 'Payment failed'
);
}
private function handleWithdrawalSuccess($payout): bool
{
$transactionId = $payout->metadata->transaction_id;
return app(TransactionService::class)->completeWithdrawal($transactionId);
}
}
Step 3: Register Processor
<?php
// config/payments.php
return [
'processors' => [
'stripe' => [
'class' => \App\Services\Payments\Processors\StripeProcessor::class,
'secret_key' => env('STRIPE_SECRET_KEY'),
'publishable_key' => env('STRIPE_PUBLISHABLE_KEY'),
'webhook_secret' => env('STRIPE_WEBHOOK_SECRET')
],
'coinbase' => [
'class' => \App\Services\Payments\Processors\CoinbaseProcessor::class,
'api_key' => env('COINBASE_API_KEY')
]
]
];
Game Provider Integration
Creating Provider Adapter
Step 1: Define Interface
<?php
namespace App\Services\Games;
interface GameProviderInterface
{
public function authenticate(): bool;
public function fetchGames(): array;
public function launchGame(int $gameId, int $userId, string $mode): string;
public function getBalance(int $userId): float;
public function processRound(array $data): array;
}
Step 2: Implement Provider
<?php
namespace App\Services\Games\Providers;
use App\Services\Games\GameProviderInterface;
class PragmaticPlayProvider implements GameProviderInterface
{
protected $apiUrl;
protected $secureLogin;
protected $securePassword;
public function __construct()
{
$this->apiUrl = config('games.pragmatic.api_url');
$this->secureLogin = config('games.pragmatic.secure_login');
$this->securePassword = config('games.pragmatic.secure_password');
}
public function authenticate(): bool
{
try {
$response = $this->makeRequest('authenticate', [
'secureLogin' => $this->secureLogin,
'securePassword' => $this->securePassword
]);
return $response['error'] === 0;
} catch (\Exception $e) {
Log::error('Pragmatic authentication failed', [
'error' => $e->getMessage()
]);
return false;
}
}
public function fetchGames(): array
{
$response = $this->makeRequest('getGameList', [
'casinoID' => config('games.pragmatic.casino_id')
]);
if ($response['error'] !== 0) {
throw new \Exception('Failed to fetch games');
}
return array_map(function($game) {
return [
'external_id' => $game['gameID'],
'name' => $game['gameName'],
'category' => $this->mapCategory($game['type']),
'thumbnail' => $game['thumbnail'],
'has_demo' => $game['freeRoundSupport'] === 1,
'rtp' => $game['rtp'] ?? null
];
}, $response['gameList']);
}
public function launchGame(int $gameId, int $userId, string $mode): string
{
$game = Game::find($gameId);
$user = User::find($userId);
$params = [
'casinoID' => config('games.pragmatic.casino_id'),
'gameID' => $game->external_id,
'playerID' => $user->id,
'playerName' => $user->username,
'currency' => $user->currency_code,
'language' => $user->language ?? 'en',
'returnURL' => route('games.close'),
'lobbyURL' => route('games.lobby')
];
if ($mode === 'demo') {
$params['mode'] = 'fun';
}
$response = $this->makeRequest('authenticate', $params);
if ($response['error'] !== 0) {
throw new \Exception($response['description']);
}
// Create session record
GameSession::create([
'user_id' => $userId,
'game_id' => $gameId,
'session_token' => $response['sessionToken'],
'mode' => $mode,
'currency_code' => $user->currency_code,
'balance_start' => $user->balance
]);
return $response['gameURL'];
}
public function getBalance(int $userId): float
{
$user = User::find($userId);
return $user->balance + $user->bonus_balance;
}
public function processRound(array $data): array
{
// Validate signature
if (!$this->validateSignature($data)) {
return $this->errorResponse('Invalid signature', 1);
}
$userId = $data['playerID'];
$roundId = $data['roundID'];
$betAmount = $data['betAmount'] ?? 0;
$winAmount = $data['winAmount'] ?? 0;
DB::beginTransaction();
try {
$user = User::lockForUpdate()->find($userId);
if (!$user) {
return $this->errorResponse('Player not found', 2);
}
// Process bet
if ($betAmount > 0) {
if ($user->balance < $betAmount) {
return $this->errorResponse('Insufficient balance', 3);
}
$user->balance -= $betAmount;
Transaction::create([
'user_id' => $userId,
'type' => 'bet',
'amount' => $betAmount,
'balance_before' => $user->balance + $betAmount,
'balance_after' => $user->balance,
'currency_code' => $user->currency_code,
'status' => 'completed',
'reference_id' => $roundId
]);
}
// Process win
if ($winAmount > 0) {
$user->balance += $winAmount;
Transaction::create([
'user_id' => $userId,
'type' => 'win',
'amount' => $winAmount,
'balance_before' => $user->balance - $winAmount,
'balance_after' => $user->balance,
'currency_code' => $user->currency_code,
'status' => 'completed',
'reference_id' => $roundId
]);
}
$user->save();
DB::commit();
return [
'error' => 0,
'balance' => $user->balance,
'currency' => $user->currency_code
];
} catch (\Exception $e) {
DB::rollBack();
Log::error('Game round processing failed', [
'user_id' => $userId,
'round_id' => $roundId,
'error' => $e->getMessage()
]);
return $this->errorResponse('Processing error', 4);
}
}
private function makeRequest(string $endpoint, array $params): array
{
$url = $this->apiUrl . '/' . $endpoint;
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($params));
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode !== 200) {
throw new \Exception("API request failed with code {$httpCode}");
}
return json_decode($response, true);
}
private function validateSignature(array $data): bool
{
$signature = $data['signature'] ?? '';
unset($data['signature']);
$string = http_build_query($data) . $this->securePassword;
$calculated = md5($string);
return hash_equals($calculated, $signature);
}
private function mapCategory(string $type): string
{
return match($type) {
'slot' => 'slots',
'table' => 'table',
'live' => 'live',
default => 'other'
};
}
private function errorResponse(string $message, int $code): array
{
return [
'error' => $code,
'description' => $message
];
}
}
Database Queries
Query Builder Examples
Basic Queries
// Select with conditions
$users = DB::table('users')
->where('status', 'active')
->where('balance', '>', 100)
->orderBy('created_at', 'desc')
->limit(10)
->get();
// Join tables
$transactions = DB::table('transactions')
->join('users', 'users.id', '=', 'transactions.user_id')
->select('transactions.*', 'users.username')
->where('transactions.status', 'completed')
->get();
// Aggregates
$totalDeposits = DB::table('transactions')
->where('type', 'deposit')
->where('status', 'completed')
->sum('amount');
// Group by
$dailyStats = DB::table('transactions')
->select(
DB::raw('DATE(created_at) as date'),
DB::raw('SUM(CASE WHEN type = "deposit" THEN amount ELSE 0 END) as deposits'),
DB::raw('SUM(CASE WHEN type = "withdrawal" THEN amount ELSE 0 END) as withdrawals')
)
->whereBetween('created_at', [$startDate, $endDate])
->groupBy('date')
->get();
Complex Queries
// Subquery
$highRollers = DB::table('users')
->whereIn('id', function($query) {
$query->select('user_id')
->from('transactions')
->where('type', 'bet')
->groupBy('user_id')
->havingRaw('SUM(amount) > ?', [10000]);
})
->get();
// Common Table Expression
$activeUsers = DB::table('game_sessions')
->select('user_id')
->where('started_at', '>=', now()->subDays(7))
->groupBy('user_id');
report = DB::table(DB::raw("({
activeUsers->toSql()}) as active"))
->mergeBindings($activeUsers)
->join('users', 'users.id', '=', 'active.user_id')
->select('users.*')
->get();
Testing
Unit Tests
Service Test Example
<?php
namespace Tests\Unit\Services;
use Tests\TestCase;
use App\Services\BonusService;
use App\Models\User;
use App\Models\Bonus;
class BonusServiceTest extends TestCase
{
protected $bonusService;
public function setUp(): void
{
parent::setUp();
$this->bonusService = app(BonusService::class);
}
public function test_can_claim_deposit_bonus()
{
$user = User::factory()->create(['balance' => 0]);
$bonus = Bonus::factory()->create([
'type' => 'deposit_match',
'percentage' => 100,
'max_bonus' => 500,
'wagering_requirement' => 35
]);
$result = $this->bonusService->claimBonus(
$user->id,
$bonus->code,
100
);
$this->assertTrue($result);
$this->assertEquals(100, $user->fresh()->bonus_balance);
}
public function test_cannot_claim_bonus_without_deposit()
{
$user = User::factory()->create();
$bonus = Bonus::factory()->create(['min_deposit' => 20]);
$result = $this->bonusService->claimBonus(
$user->id,
$bonus->code,
10
);
$this->assertFalse($result);
}
public function test_bonus_wagering_calculation()
{
$user = User::factory()->create();
$userBonus = UserBonus::factory()->create([
'user_id' => $user->id,
'bonus_amount' => 100,
'wagering_requirement' => 35
]);
$required = $this->bonusService->calculateWageringRequired($userBonus->id);
$this->assertEquals(3500, $required);
}
}
Integration Tests
API Endpoint Test
<?php
namespace Tests\Feature\API;
use Tests\TestCase;
use App\Models\User;
class GameAPITest extends TestCase
{
protected $user;
protected $token;
public function setUp(): void
{
parent::setUp();
$this->user = User::factory()->create();
$this->token = $this->user->createToken('test')->plainTextToken;
}
public function test_can_list_games()
{
$response = $this->withHeaders([
'Authorization' => 'Bearer ' . $this->token
])->getJson('/api/v1/games');
$response->assertStatus(200)
->assertJsonStructure([
'status',
'data' => [
'games' => [
'*' => [
'id',
'name',
'provider',
'category'
]
]
]
]);
}
public function test_can_launch_game()
{
$game = Game::factory()->create();
$response = $this->withHeaders([
'Authorization' => 'Bearer ' . $this->token
])->postJson('/api/v1/games/' . $game->id . '/launch', [
'mode' => 'real'
]);
$response->assertStatus(200)
->assertJsonStructure([
'status',
'data' => [
'session_id',
'launch_url'
]
]);
}
public function test_cannot_launch_game_without_balance()
{
$this->user->update(['balance' => 0]);
$game = Game::factory()->create(['min_bet' => 1]);
$response = $this->withHeaders([
'Authorization' => 'Bearer ' . $this->token
])->postJson('/api/v1/games/' . $game->id . '/launch', [
'mode' => 'real'
]);
$response->assertStatus(400);
}
}
Debugging
Logging
Application Logs
// Different log levels
Log::debug('Debug information', ['data' => $data]);
Log::info('User logged in', ['user_id' => $userId]);
Log::warning('Low balance detected', ['balance' => $balance]);
Log::error('Payment failed', ['error' => $exception->getMessage()]);
Log::critical('Database connection lost');
// Custom channels
Log::channel('payments')->info('Payment processed', $data);
Log::channel('games')->error('Game launch failed', $error);
Performance Profiling
Query Logging
DB::enableQueryLog();
// Your code here
$queries = DB::getQueryLog();
foreach ($queries as $query) {
Log::debug('Query', [
'sql' => $query['query'],
'time' => $query['time']
]);
}
Deployment
Production Checklist
Configuration
# Set environment to production
APP_ENV=production
APP_DEBUG=false
Enable optimizations
php artisan config:cache
php artisan route:cache
php artisan view:cache
Set proper permissions
chmod -R 755 /var/www/casino
chmod -R 775 /var/www/casino/storage
chmod -R 775 /var/www/casino/bootstrap/cache
Enable opcache
opcache.enable=1
opcache.memory_consumption=256
opcache.max_accelerated_files=20000
Zero-Downtime Deployment
Deployment Script
#!/bin/bash
Enable maintenance mode
php artisan down
Pull latest code
git pull origin main
Install dependencies
composer install --no-dev --optimize-autoloader
npm install --production
npm run build
Run migrations
php artisan migrate --force
Clear and rebuild caches
php artisan config:cache
php artisan route:cache
php artisan view:cache
Restart services
sudo systemctl restart php8.1-fpm
sudo systemctl restart nginx
Disable maintenance mode
php artisan up
echo "Deployment completed successfully"