Laravel Deployment: A Step-by-Step Guide for Shared Hosting and VPS
Deploy your Laravel application to production environments.
In the world of Laravel development, writing code that works is just the beginning. The true mark of a professional Laravel developer is writing code that's clean, maintainable, and scalable. This comprehensive guide covers the essential best practices that will transform your Laravel applications from functional to exceptional.
Why DDD?
Domain-Driven Design helps you structure your application around business domains, making it more maintainable and scalable.
Implementation:
// Traditional structure (MVC)
app/
├── Http/
│ ├── Controllers/
│ └── Requests/
├── Models/
└── Services/
// DDD structure
app/
├── Domain/
│ ├── Users/
│ │ ├── Actions/
│ │ ├── Models/
│ │ ├── DataTransferObjects/
│ │ ├── Events/
│ │ ├── Listeners/
│ │ ├── Rules/
│ │ └── ValueObjects/
│ └── Products/
│ ├── Actions/
│ └── ...
├── Application/
│ ├── Users/
│ │ ├── Services/
│ │ └── Queries/
│ └── Products/
└── Infrastructure/
├── Http/
├── Database/
└── Providers/
Example Domain Structure:
// app/Domain/Users/Models/User.php
namespace App\Domain\Users\Models;
use Illuminate\Database\Eloquent\Model;
use App\Domain\Users\ValueObjects\Email;
use App\Domain\Users\Events\UserCreated;
class User extends Model
{
protected $dispatchesEvents = [
'created' => UserCreated::class,
];
public function setEmailAttribute($value)
{
$this->attributes['email'] = (new Email($value))->value();
}
}
// app/Domain/Users/ValueObjects/Email.php
namespace App\Domain\Users\ValueObjects;
class Email
{
private string $value;
public function __construct(string $email)
{
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new \InvalidArgumentException('Invalid email address');
}
$this->value = strtolower(trim($email));
}
public function value(): string
{
return $this->value;
}
public function domain(): string
{
return explode('@', $this->value)[1];
}
}
// app/Domain/Users/Actions/CreateUser.php
namespace App\Domain\Users\Actions;
use App\Domain\Users\Models\User;
use App\Domain\Users\DataTransferObjects\UserData;
use Illuminate\Support\Facades\Hash;
class CreateUser
{
public function execute(UserData $userData): User
{
$user = new User([
'name' => $userData->name,
'email' => $userData->email,
'password' => Hash::make($userData->password),
]);
$user->save();
return $user;
}
}
Why Repositories?
Repositories abstract data access, making your code more testable and flexible.
Implementation:
// app/Domain/Users/Contracts/UserRepositoryInterface.php
namespace App\Domain\Users\Contracts;
use App\Domain\Users\Models\User;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
interface UserRepositoryInterface
{
public function find(int $id): ?User;
public function findByEmail(string $email): ?User;
public function all(): array;
public function paginate(int $perPage = 15): LengthAwarePaginator;
public function create(array $data): User;
public function update(User $user, array $data): bool;
public function delete(User $user): bool;
public function withTrashed(): self;
}
// app/Infrastructure/Database/Repositories/UserRepository.php
namespace App\Infrastructure\Database\Repositories;
use App\Domain\Users\Contracts\UserRepositoryInterface;
use App\Domain\Users\Models\User;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
class UserRepository implements UserRepositoryInterface
{
protected $model;
public function __construct(User $user)
{
$this->model = $user;
}
public function find(int $id): ?User
{
return $this->model->find($id);
}
public function findByEmail(string $email): ?User
{
return $this->model->where('email', $email)->first();
}
public function all(): array
{
return $this->model->all()->toArray();
}
public function paginate(int $perPage = 15): LengthAwarePaginator
{
return $this->model->paginate($perPage);
}
public function create(array $data): User
{
return $this->model->create($data);
}
public function update(User $user, array $data): bool
{
return $user->update($data);
}
public function delete(User $user): bool
{
return $user->delete();
}
public function withTrashed(): self
{
$this->model = $this->model->withTrashed();
return $this;
}
}
// app/Infrastructure/Providers/RepositoryServiceProvider.php
namespace App\Infrastructure\Providers;
use Illuminate\Support\ServiceProvider;
use App\Domain\Users\Contracts\UserRepositoryInterface;
use App\Infrastructure\Database\Repositories\UserRepository;
class RepositoryServiceProvider extends ServiceProvider
{
public function register()
{
$this->app->bind(
UserRepositoryInterface::class,
UserRepository::class
);
}
}
Why Services?
Services encapsulate business logic, keeping controllers thin and focused.
Implementation:
// app/Application/Users/Services/UserRegistrationService.php
namespace App\Application\Users\Services;
use App\Domain\Users\Contracts\UserRepositoryInterface;
use App\Domain\Users\Actions\CreateUser;
use App\Domain\Users\Actions\SendWelcomeEmail;
use App\Domain\Users\DataTransferObjects\UserRegistrationData;
class UserRegistrationService
{
private $userRepository;
private $createUser;
private $sendWelcomeEmail;
public function __construct(
UserRepositoryInterface $userRepository,
CreateUser $createUser,
SendWelcomeEmail $sendWelcomeEmail
) {
$this->userRepository = $userRepository;
$this->createUser = $createUser;
$this->sendWelcomeEmail = $sendWelcomeEmail;
}
public function register(UserRegistrationData $data): array
{
// Validate unique email
if ($this->userRepository->findByEmail($data->email)) {
throw new \Exception('Email already registered');
}
// Create user
$user = $this->createUser->execute($data);
// Send welcome email
$this->sendWelcomeEmail->execute($user);
// Log registration
\Log::info('User registered', ['user_id' => $user->id]);
return [
'user' => $user,
'token' => $user->createToken('auth-token')->plainTextToken,
];
}
public function registerWithSocial(string $provider, array $userData): User
{
// Find or create user from social provider
$user = $this->userRepository->findByEmail($userData['email']);
if (!$user) {
$user = $this->userRepository->create([
'name' => $userData['name'],
'email' => $userData['email'],
'provider' => $provider,
'provider_id' => $userData['id'],
'email_verified_at' => now(),
]);
}
return $user;
}
}
Bad Practice:
class UserController extends Controller
{
public function store(Request $request)
{
// Validate
$validated = $request->validate([
'name' => 'required',
'email' => 'required|email|unique:users',
'password' => 'required|min:8',
]);
// Create user
$user = User::create([
'name' => $validated['name'],
'email' => $validated['email'],
'password' => Hash::make($validated['password']),
]);
// Send welcome email
Mail::to($user->email)->send(new WelcomeEmail($user));
// Create user profile
$user->profile()->create([
'bio' => 'New user',
'avatar' => 'default.jpg',
]);
// Log activity
ActivityLog::create([
'user_id' => $user->id,
'action' => 'user_registered',
'details' => 'New user registered',
]);
// Return response
return redirect()->route('users.show', $user)
->with('success', 'User created successfully');
}
}
Good Practice (SRP Applied):
// Controller (Single responsibility: Handle HTTP)
class UserController extends Controller
{
private $registrationService;
public function __construct(UserRegistrationService $registrationService)
{
$this->registrationService = $registrationService;
}
public function store(UserRegistrationRequest $request)
{
$result = $this->registrationService->register(
UserRegistrationData::fromRequest($request)
);
return response()->json($result, 201);
}
}
// Request (Single responsibility: Validate)
class UserRegistrationRequest extends FormRequest
{
public function rules()
{
return [
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users',
'password' => 'required|min:8|confirmed',
];
}
}
// Data Transfer Object (Single responsibility: Data structure)
class UserRegistrationData
{
public string $name;
public string $email;
public string $password;
public static function fromRequest(UserRegistrationRequest $request): self
{
$data = new self();
$data->name = $request->input('name');
$data->email = $request->input('email');
$data->password = $request->input('password');
return $data;
}
}
// Service (Single responsibility: Business logic)
class UserRegistrationService
{
public function register(UserRegistrationData $data): array
{
$user = $this->createUser->execute($data);
$this->sendWelcomeEmail->execute($user);
$this->createUserProfile->execute($user);
$this->logRegistration->execute($user);
return [
'user' => $user,
'token' => $user->createToken('auth-token')->plainTextToken,
];
}
}
Implementation:
// app/Domain/Payments/Contracts/PaymentGatewayInterface.php
interface PaymentGatewayInterface
{
public function charge(float $amount, array $options = []): PaymentResult;
public function refund(string $transactionId, float $amount): bool;
}
// Concrete implementations
class StripePaymentGateway implements PaymentGatewayInterface
{
public function charge(float $amount, array $options = []): PaymentResult
{
// Stripe-specific implementation
}
}
class PayPalPaymentGateway implements PaymentGatewayInterface
{
public function charge(float $amount, array $options = []): PaymentResult
{
// PayPal-specific implementation
}
}
class BankTransferGateway implements PaymentGatewayInterface
{
public function charge(float $amount, array $options = []): PaymentResult
{
// Bank transfer implementation
}
}
// Payment service (Open for extension, closed for modification)
class PaymentService
{
private $gateway;
public function __construct(PaymentGatewayInterface $gateway)
{
$this->gateway = $gateway;
}
public function processOrder(Order $order): PaymentResult
{
return $this->gateway->charge(
$order->total,
['order_id' => $order->id]
);
}
}
// Factory for creating gateways
class PaymentGatewayFactory
{
public static function create(string $gateway): PaymentGatewayInterface
{
return match($gateway) {
'stripe' => new StripePaymentGateway(),
'paypal' => new PayPalPaymentGateway(),
'bank_transfer' => new BankTransferGateway(),
default => throw new \InvalidArgumentException("Unknown gateway: $gateway"),
};
}
}
// Usage
$gateway = PaymentGatewayFactory::create('stripe');
$paymentService = new PaymentService($gateway);
$result = $paymentService->processOrder($order);
// Adding new gateway doesn't require changing existing code
class CryptoPaymentGateway implements PaymentGatewayInterface
{
public function charge(float $amount, array $options = []): PaymentResult
{
// New cryptocurrency implementation
}
}
Implementation:
// Base interface
interface NotificationInterface
{
public function send(User $user, string $message): bool;
}
// Implementations
class EmailNotification implements NotificationInterface
{
public function send(User $user, string $message): bool
{
// Send email
return Mail::to($user->email)
->send(new GenericNotification($message));
}
}
class SMSNotification implements NotificationInterface
{
public function send(User $user, string $message): bool
{
// Send SMS
return $this->smsService->send($user->phone, $message);
}
}
class PushNotification implements NotificationInterface
{
public function send(User $user, string $message): bool
{
// Send push notification
return $user->devices->each->sendPush($message);
}
}
// Notification service using LSP
class NotificationService
{
private $notifications;
public function __construct(NotificationInterface ...$notifications)
{
$this->notifications = $notifications;
}
public function notify(User $user, string $message): void
{
foreach ($this->notifications as $notification) {
try {
$notification->send($user, $message);
} catch (\Exception $e) {
\Log::error('Notification failed', [
'type' => get_class($notification),
'error' => $e->getMessage(),
]);
}
}
}
}
// Usage - all implementations are interchangeable
$service = new NotificationService(
new EmailNotification(),
new SMSNotification(),
new PushNotification()
);
$service->notify($user, 'Your order has shipped!');
Bad Practice (Fat Interface):
interface UserRepositoryInterface
{
public function find(int $id);
public function findByEmail(string $email);
public function create(array $data);
public function update(int $id, array $data);
public function delete(int $id);
public function restore(int $id);
public function forceDelete(int $id);
public function paginate(int $perPage);
public function withTrashed();
public function onlyTrashed();
public function attachRole(int $userId, int $roleId);
public function detachRole(int $userId, int $roleId);
// ... 20 more methods
}
Good Practice (Segregated Interfaces):
// Core repository
interface RepositoryInterface
{
public function find(int $id);
public function findByEmail(string $email);
public function create(array $data);
public function update(int $id, array $data);
public function delete(int $id);
public function paginate(int $perPage);
}
// Soft delete operations
interface SoftDeletesRepositoryInterface
{
public function restore(int $id);
public function forceDelete(int $id);
public function withTrashed();
public function onlyTrashed();
}
// Role operations
interface RoleRepositoryInterface
{
public function attachRole(int $userId, int $roleId);
public function detachRole(int $userId, int $roleId);
public function syncRoles(int $userId, array $roleIds);
}
// User repository implementing multiple interfaces
class UserRepository implements
RepositoryInterface,
SoftDeletesRepositoryInterface,
RoleRepositoryInterface
{
// Implement all methods
}
// Service needing only basic operations
class UserLookupService
{
public function __construct(RepositoryInterface $repository)
{
$this->repository = $repository;
}
public function findUser(int $id)
{
return $this->repository->find($id);
}
}
// Service needing soft delete operations
class UserAdminService
{
public function __construct(
RepositoryInterface $repository,
SoftDeletesRepositoryInterface $softDeleteRepository
) {
$this->repository = $repository;
$this->softDeleteRepository = $softDeleteRepository;
}
}
Implementation:
// High-level module
class OrderProcessor
{
private $paymentGateway;
private $notificationService;
private $orderRepository;
public function __construct(
PaymentGatewayInterface $paymentGateway,
NotificationInterface $notificationService,
OrderRepositoryInterface $orderRepository
) {
$this->paymentGateway = $paymentGateway;
$this->notificationService = $notificationService;
$this->orderRepository = $orderRepository;
}
public function process(Order $order): ProcessResult
{
// Process payment
$paymentResult = $this->paymentGateway->charge($order->total);
if ($paymentResult->success) {
$order->status = 'paid';
$this->orderRepository->save($order);
// Send notification
$this->notificationService->send(
$order->user,
'Your order has been processed'
);
return ProcessResult::success($order);
}
return ProcessResult::failure($paymentResult->error);
}
}
// Service provider binding abstractions to implementations
class AppServiceProvider extends ServiceProvider
{
public function register()
{
// Bind interfaces to implementations
$this->app->bind(
PaymentGatewayInterface::class,
StripePaymentGateway::class
);
$this->app->bind(
NotificationInterface::class,
EmailNotification::class
);
$this->app->bind(
OrderRepositoryInterface::class,
EloquentOrderRepository::class
);
}
}
// Testing with different implementations
class TestOrderProcessor extends TestCase
{
public function test_order_processing()
{
// Use mock implementations
$mockPaymentGateway = $this->createMock(PaymentGatewayInterface::class);
$mockNotification = $this->createMock(NotificationInterface::class);
$mockRepository = $this->createMock(OrderRepositoryInterface::class);
$processor = new OrderProcessor(
$mockPaymentGateway,
$mockNotification,
$mockRepository
);
// Test with mocked dependencies
}
}
Bad Naming:
class x {
public function y($a, $b) {
$c = $a + $b;
$d = $c * 10;
return $d;
}
}
function proc($d) {
// What does 'd' mean?
}
$temp = getUser(); // Temporary what?
$flag = true; // Flag for what?
Good Naming:
class ShoppingCart {
public function calculateTotalWithTax(float $subtotal, float $taxRate): float {
$taxAmount = $subtotal * $taxRate;
$totalAmount = $subtotal + $taxAmount;
return $totalAmount;
}
}
function processOrder(Order $order): void {
// Clear intention
}
$user = getAuthenticatedUser();
$isUserActive = $user->isActive();
$hasValidSubscription = $user->subscription()->valid();
Naming Guidelines:
// Classes: Nouns, PascalCase
class OrderProcessor {}
class UserRepository {}
class EmailNotificationService {}
// Methods: Verbs, camelCase
public function calculateTotal() {}
public function sendNotification() {}
public function validateInput() {}
// Variables: Descriptive, camelCase
$orderTotal = 100.50;
$customerEmail = 'john@example.com';
$isPaymentSuccessful = true;
// Constants: UPPER_SNAKE_CASE
const MAX_LOGIN_ATTEMPTS = 5;
const DEFAULT_PAGINATION_SIZE = 25;
// Boolean variables: prefix with is/has/can/should
$isActive = true;
$hasPermission = false;
$canEdit = true;
$shouldNotify = false;
// Collections: plural
$users = User::all();
$orders = $customer->orders;
// Single items: singular
$user = User::find(1);
$order = Order::latest()->first();
Bad Function:
public function process($data) {
// Too many responsibilities
if ($data) {
$user = User::find($data['user_id']);
if ($user) {
$order = Order::create([
'user_id' => $user->id,
'total' => $data['total'],
'items' => $data['items'],
]);
if ($order) {
Mail::to($user->email)->send(new OrderConfirmation($order));
$log = ActivityLog::create([
'user_id' => $user->id,
'action' => 'order_created',
'details' => json_encode($order),
]);
return ['success' => true, 'order' => $order];
}
}
}
return ['success' => false, 'error' => 'Failed'];
}
Good Function (Extracted Responsibilities):
public function createOrderFromRequest(OrderRequest $request): OrderCreationResult
{
$user = $this->findUser($request->userId());
$this->validateUserCanOrder($user);
$order = $this->createOrder($user, $request->items(), $request->total());
$this->sendOrderConfirmation($user, $order);
$this->logOrderActivity($user, $order);
return OrderCreationResult::success($order);
}
private function findUser(int $userId): User
{
$user = User::find($userId);
if (!$user) {
throw new UserNotFoundException("User {$userId} not found");
}
return $user;
}
private function validateUserCanOrder(User $user): void
{
if (!$user->isActive()) {
throw new UserNotActiveException("User {$user->id} is not active");
}
if ($user->hasExceededOrderLimit()) {
throw new OrderLimitExceededException("Order limit exceeded");
}
}
private function createOrder(User $user, array $items, float $total): Order
{
return DB::transaction(function () use ($user, $items, $total) {
$order = Order::create([
'user_id' => $user->id,
'total' => $total,
'status' => 'pending',
]);
foreach ($items as $item) {
$order->items()->create([
'product_id' => $item['product_id'],
'quantity' => $item['quantity'],
'price' => $item['price'],
]);
}
return $order;
});
}
private function sendOrderConfirmation(User $user, Order $order): void
{
try {
Mail::to($user->email)
->send(new OrderConfirmation($order));
} catch (\Exception $e) {
\Log::warning('Failed to send order confirmation', [
'order_id' => $order->id,
'error' => $e->getMessage(),
]);
}
}
private function logOrderActivity(User $user, Order $order): void
{
ActivityLog::create([
'user_id' => $user->id,
'action' => 'order_created',
'details' => [
'order_id' => $order->id,
'total' => $order->total,
'item_count' => $order->items->count(),
],
]);
}
Project Structure Guidelines:
app/
├── Console/
│ ├── Commands/
│ │ ├── Import/
│ │ │ ├── ImportUsers.php
│ │ │ └── ImportProducts.php
│ │ └── Reports/
│ │ ├── GenerateSalesReport.php
│ │ └── GenerateUserReport.php
│ └── Kernel.php
├── Domain/ # Business logic layer
│ ├── Users/
│ │ ├── Actions/ # Single-use actions
│ │ │ ├── CreateUser.php
│ │ │ ├── UpdateUser.php
│ │ │ └── DeleteUser.php
│ │ ├── Collections/ # Custom collections
│ │ │ └── UserCollection.php
│ │ ├── DataTransferObjects/ # DTOs
│ │ │ ├── UserData.php
│ │ │ └── UserRegistrationData.php
│ │ ├── Events/ # Domain events
│ │ │ ├── UserCreated.php
│ │ │ └── UserUpdated.php
│ │ ├── Exceptions/ # Domain exceptions
│ │ │ ├── UserNotFoundException.php
│ │ │ └── InvalidUserException.php
│ │ ├── Listeners/ # Event listeners
│ │ │ ├── SendWelcomeEmail.php
│ │ │ └── LogUserActivity.php
│ │ ├── Models/ # Eloquent models
│ │ │ ├── User.php
│ │ │ └── Profile.php
│ │ ├── Observers/ # Model observers
│ │ │ └── UserObserver.php
│ │ ├── Policies/ # Authorization policies
│ │ │ └── UserPolicy.php
│ │ ├── Rules/ # Validation rules
│ │ │ ├── UniqueUserEmail.php
│ │ │ └── ValidUserRole.php
│ │ ├── Scopes/ # Query scopes
│ │ │ ├── ActiveScope.php
│ │ │ └── WithRoleScope.php
│ │ └── ValueObjects/ # Value objects
│ │ ├── Email.php
│ │ └── Password.php
│ ├── Products/
│ │ └── ... (similar structure)
│ └── Orders/
│ └── ... (similar structure)
├── Application/ # Application services layer
│ ├── Users/
│ │ ├── Queries/ # Query classes
│ │ │ ├── GetActiveUsers.php
│ │ │ └── GetUsersWithOrders.php
│ │ ├── Services/ # Application services
│ │ │ ├── UserRegistrationService.php
│ │ │ └── UserProfileService.php
│ │ └── ViewModels/ # View models
│ │ └── UserProfileViewModel.php
│ └── Orders/
│ └── ... (similar structure)
├── Infrastructure/ # Framework-specific implementations
│ ├── Http/
│ │ ├── Controllers/
│ │ │ ├── Api/
│ │ │ └── V1/ # API versioning
│ │ │ ├── UserController.php
│ │ │ └── OrderController.php
│ │ └── Web/
│ │ ├── UserController.php
│ │ └── OrderController.php
│ ├── Middleware/
│ │ ├── Api/
│ │ │ ├── Authenticate.php
│ │ │ └── ThrottleRequests.php
│ │ └── Web/
│ │ ├── Authenticate.php
│ │ └── TrimStrings.php
│ ├── Requests/
│ │ ├── Api/
│ │ │ └── V1/
│ │ │ ├── User/
│ │ │ │ ├── StoreRequest.php
│ │ │ │ └── UpdateRequest.php
│ │ │ └── Order/
│ │ │ ├── StoreRequest.php
│ │ │ └── UpdateRequest.php
│ │ └── Web/
│ │ └── ... (similar structure)
│ ├── Resources/
│ │ ├── Api/
│ │ │ └── V1/
│ │ │ ├── UserResource.php
│ │ │ └── OrderResource.php
│ │ └── Web/
│ │ └── ... (similar structure)
│ ├── Database/
│ │ ├── Factories/
│ │ │ ├── UserFactory.php
│ │ │ └── OrderFactory.php
│ │ ├── Migrations/
│ │ │ ├── 2023_01_01_000000_create_users_table.php
│ │ │ └── 2023_01_01_000001_create_orders_table.php
│ │ ├── Seeders/
│ │ │ ├── DatabaseSeeder.php
│ │ │ ├── UserSeeder.php
│ │ │ └── OrderSeeder.php
│ │ └── Repositories/
│ │ ├── UserRepository.php
│ │ └── OrderRepository.php
│ ├── Mail/
│ │ ├── WelcomeEmail.php
│ │ └── OrderConfirmation.php
│ ├── Notifications/
│ │ ├── OrderShipped.php
│ │ └── PasswordReset.php
│ └── Providers/
│ ├── AppServiceProvider.php
│ ├── AuthServiceProvider.php
│ ├── EventServiceProvider.php
│ ├── RepositoryServiceProvider.php
│ └── RouteServiceProvider.php
├── Support/ # Helper classes
│ ├── Helpers/
│ │ ├── DateHelper.php
│ │ └── StringHelper.php
│ ├── Traits/
│ │ ├── HasUuid.php
│ │ └── LogsActivity.php
│ └── Collections/
│ └── PaginatedCollection.php
└── Exceptions/
└── Handler.php
Bad Comments:
// Get users
$users = User::all(); // Gets all users
// Loop through users
foreach ($users as $user) {
// Send email
Mail::send(...); // Sends email
}
Good Comments (Documenting WHY, not WHAT):
/**
* Fetches active users who have made a purchase in the last 30 days.
* This is used for the targeted marketing campaign that requires
* recently active customers to maximize conversion rates.
*
* @param int $minimumPurchase Minimum purchase amount in cents
* @param DateTimeInterface $since Only include purchases after this date
* @return UserCollection Collection of active purchasing users
* @throws DatabaseConnectionException If database connection fails
*/
public function getActivePurchasingUsers(
int $minimumPurchase = 1000,
DateTimeInterface $since = null
): UserCollection {
$since = $since ?? now()->subDays(30);
return User::query()
->where('active', true)
->whereHas('orders', function ($query) use ($minimumPurchase, $since) {
$query->where('total', '>=', $minimumPurchase)
->where('created_at', '>=', $since);
})
->with(['orders' => function ($query) use ($since) {
$query->where('created_at', '>=', $since)
->orderBy('created_at', 'desc');
}])
->get();
}
// Exception for complex business logic
if ($user->subscription->isTrial() && $user->loginCount > 5) {
// Trial users with more than 5 logins are likely engaged
// and should see premium features to encourage conversion
$showPremiumFeatures = true;
}
// TODO comments for future improvements
// TODO: Implement caching for this query as it's called frequently
// TODO: Add support for multiple currency conversions
// TODO: Consider moving this to a queued job for better performance
// FIXME comments for known issues
// FIXME: This workaround handles timezone issues until the API is fixed
// FIXME: Remove this temporary fix after v2.1 release
// NOTE comments for important information
// NOTE: This method assumes the user's email is verified
// NOTE: Cache invalidation happens automatically via model events
PHPDoc Standards:
/**
* Processes an order and handles payment, inventory, and notifications.
*
* @param Order $order The order to process
* @param PaymentMethod $paymentMethod Payment method to use
* @param bool $sendNotifications Whether to send customer notifications
* @return ProcessResult Result containing success status and any errors
* @throws PaymentFailedException If payment processing fails
* @throws InsufficientInventoryException If items are out of stock
* @throws OrderAlreadyProcessedException If order was already processed
*
* @example
* $result = $orderProcessor->process($order, $paymentMethod);
* if ($result->success) {
* echo "Order processed successfully!";
* }
*/
public function process(
Order $order,
PaymentMethod $paymentMethod,
bool $sendNotifications = true
): ProcessResult {
// Implementation
}
Well-structured Model:
namespace App\Domain\Users\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use App\Domain\Users\Collections\UserCollection;
use App\Domain\Users\Scopes\ActiveScope;
use App\Domain\Users\Traits\HasPermissions;
use App\Domain\Users\Events\UserCreated;
use App\Domain\Users\Events\UserUpdated;
class User extends Model
{
use HasPermissions;
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'name',
'email',
'password',
'active',
'email_verified_at',
];
/**
* The attributes that should be hidden for arrays.
*
* @var array
*/
protected $hidden = [
'password',
'remember_token',
'two_factor_secret',
'two_factor_recovery_codes',
];
/**
* The attributes that should be cast.
*
* @var array
*/
protected $casts = [
'email_verified_at' => 'datetime',
'active' => 'boolean',
'settings' => 'array',
'last_login_at' => 'datetime',
'metadata' => 'object',
];
/**
* The event map for the model.
*
* @var array
*/
protected $dispatchesEvents = [
'created' => UserCreated::class,
'updated' => UserUpdated::class,
];
/**
* The relationships that should always be loaded.
*
* @var array
*/
protected $with = [
'profile',
];
/**
* Create a new Eloquent Collection instance.
*
* @param array $models
* @return UserCollection
*/
public function newCollection(array $models = []): UserCollection
{
return new UserCollection($models);
}
/**
* Boot the model.
*
* @return void
*/
protected static function booted(): void
{
static::addGlobalScope(new ActiveScope);
static::creating(function ($user) {
$user->uuid = (string) \Str::uuid();
});
static::updating(function ($user) {
if ($user->isDirty('email')) {
$user->email_verified_at = null;
}
});
}
/**
* Scope a query to only include popular users.
*
* @param Builder $query
* @param int $minFollowers
* @return Builder
*/
public function scopePopular(Builder $query, int $minFollowers = 1000): Builder
{
return $query->where('followers_count', '>=', $minFollowers);
}
/**
* Scope a query to only include users with a specific role.
*
* @param Builder $query
* @param string $role
* @return Builder
*/
public function scopeWithRole(Builder $query, string $role): Builder
{
return $query->whereHas('roles', function ($q) use ($role) {
$q->where('name', $role);
});
}
/**
* Get the user's full name.
*
* @return string
*/
public function getFullNameAttribute(): string
{
return "{$this->first_name} {$this->last_name}";
}
/**
* Set the user's password.
*
* @param string $value
* @return void
*/
public function setPasswordAttribute(string $value): void
{
$this->attributes['password'] = bcrypt($value);
}
/**
* Determine if the user is an administrator.
*
* @return bool
*/
public function isAdministrator(): bool
{
return $this->hasRole('administrator');
}
/**
* Determine if the user has verified their email.
*
* @return bool
*/
public function hasVerifiedEmail(): bool
{
return !is_null($this->email_verified_at);
}
/**
* Get the user's profile.
*
* @return HasOne
*/
public function profile(): HasOne
{
return $this->hasOne(Profile::class);
}
/**
* Get the user's orders.
*
* @return HasMany
*/
public function orders(): HasMany
{
return $this->hasMany(Order::class);
}
/**
* Get the user's roles.
*
* @return BelongsToMany
*/
public function roles(): BelongsToMany
{
return $this->belongsToMany(Role::class)
->withTimestamps()
->withPivot('assigned_by');
}
}
Common N+1 Problem & Solutions:
// ❌ BAD: N+1 Query Problem
$users = User::all();
foreach ($users as $user) {
echo $user->profile->bio; // Executes query for each user
}
// ✅ GOOD: Eager Loading
$users = User::with('profile')->get();
foreach ($users as $user) {
echo $user->profile->bio; // No additional queries
}
// ✅ BETTER: Eager load with constraints
$users = User::with([
'profile',
'orders' => function ($query) {
$query->where('status', 'completed')
->orderBy('created_at', 'desc')
->limit(5);
},
'orders.items',
])->get();
// ✅ BEST: Lazy eager loading when needed
$users = User::all();
$users->load(['profile', 'orders']);
// Advanced eager loading patterns
class UserController extends Controller
{
public function index()
{
// Load counts without loading relationships
$users = User::withCount(['orders', 'posts'])
->with(['profile' => function ($query) {
$query->select('id', 'user_id', 'bio');
}])
->paginate(25);
return UserResource::collection($users);
}
public function show(User $user)
{
// Load nested relationships
$user->load([
'profile',
'orders.items.product',
'posts.comments.user',
'notifications' => function ($query) {
$query->where('read', false)
->orderBy('created_at', 'desc');
},
]);
return new UserResource($user);
}
}
// Using subqueries for optimization
$users = User::addSelect([
'last_order_date' => Order::select('created_at')
->whereColumn('user_id', 'users.id')
->latest()
->limit(1),
'total_spent' => Order::selectRaw('SUM(total)')
->whereColumn('user_id', 'users.id')
->where('status', 'completed'),
])->get();
Comprehensive Migration Example:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up(): void
{
Schema::create('users', function (Blueprint $table) {
// Primary key
$table->id();
$table->uuid('uuid')->unique();
// Basic information
$table->string('first_name', 100);
$table->string('last_name', 100);
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->rememberToken();
// Profile information
$table->string('phone', 20)->nullable();
$table->date('date_of_birth')->nullable();
$table->enum('gender', ['male', 'female', 'other'])->nullable();
// Status flags
$table->boolean('active')->default(true);
$table->boolean('is_suspended')->default(false);
$table->timestamp('suspended_until')->nullable();
// Preferences
$table->string('timezone', 50)->default('UTC');
$table->string('locale', 10)->default('en');
$table->json('preferences')->nullable();
$table->json('metadata')->nullable();
// Statistics
$table->integer('login_count')->default(0);
$table->timestamp('last_login_at')->nullable();
$table->ipAddress('last_login_ip')->nullable();
// Indexes
$table->index(['active', 'created_at']);
$table->index('email_verified_at');
$table->index('last_login_at');
$table->index(['first_name', 'last_name']);
// Full-text search
$table->fullText(['first_name', 'last_name', 'email']);
// Timestamps
$table->timestamps();
$table->softDeletes();
// Additional comments
$table->comment('Stores user account information');
});
// Create profile table with foreign key
Schema::create('profiles', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')
->constrained()
->onUpdate('cascade')
->onDelete('cascade');
$table->text('bio')->nullable();
$table->string('avatar_url')->nullable();
$table->string('website')->nullable();
$table->json('social_links')->nullable();
$table->timestamps();
// Unique constraint
$table->unique('user_id');
});
// Create many-to-many pivot table
Schema::create('role_user', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')
->constrained()
->onUpdate('cascade')
->onDelete('cascade');
$table->foreignId('role_id')
->constrained()
->onUpdate('cascade')
->onDelete('cascade');
$table->foreignId('assigned_by')
->nullable()
->constrained('users')
->nullOnDelete();
$table->timestamp('expires_at')->nullable();
$table->text('notes')->nullable();
$table->timestamps();
// Composite unique with additional condition
$table->unique(['user_id', 'role_id', 'expires_at']);
// Partial index
$table->index(['expires_at'], 'idx_active_roles')
->where('expires_at', '>', DB::raw('NOW()'));
});
// Add column with default value
Schema::table('users', function (Blueprint $table) {
$table->string('display_name')
->virtualAs("CONCAT(first_name, ' ', last_name)")
->after('last_name');
});
// Create stored procedure for complex operations
DB::unprepared('
CREATE PROCEDURE CalculateUserStats(IN userId INT)
BEGIN
SELECT
COUNT(DISTINCT o.id) as order_count,
SUM(o.total) as total_spent,
AVG(o.total) as avg_order_value,
MAX(o.created_at) as last_order_date
FROM orders o
WHERE o.user_id = userId
AND o.status = "completed";
END
');
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down(): void
{
// Drop in reverse order
DB::unprepared('DROP PROCEDURE IF EXISTS CalculateUserStats');
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('display_name');
});
Schema::dropIfExists('role_user');
Schema::dropIfExists('profiles');
Schema::dropIfExists('users');
}
};
Test Organization:
tests/
├── Unit/ # Unit tests (isolated)
│ ├── Domain/
│ │ ├── Users/
│ │ │ ├── Actions/
│ │ │ │ ├── CreateUserTest.php
│ │ │ │ └── UpdateUserTest.php
│ │ │ ├── Models/
│ │ │ │ └── UserTest.php
│ │ │ ├── ValueObjects/
│ │ │ │ └── EmailTest.php
│ │ │ └── Rules/
│ │ │ └── UniqueUserEmailTest.php
│ │ └── Products/
│ │ └── ...
│ └── Support/
│ └── HelpersTest.php
├── Feature/ # Feature tests (with framework)
│ ├── Api/
│ │ └── V1/
│ │ ├── AuthenticationTest.php
│ │ ├── UserTest.php
│ │ └── OrderTest.php
│ ├── Web/
│ │ ├── AuthenticationTest.php
│ │ ├── UserTest.php
│ │ └── OrderTest.php
│ └── Console/
│ ├── ImportUsersTest.php
│ └── GenerateReportTest.php
├── Integration/ # Integration tests
│ ├── Database/
│ │ ├── UserRepositoryTest.php
│ │ └── OrderRepositoryTest.php
│ └── Services/
│ ├── PaymentServiceTest.php
│ └── NotificationServiceTest.php
├── Browser/ # Dusk tests
│ ├── AuthenticationTest.php
│ └── CheckoutTest.php
└── TestCase.php
Unit Test Example:
<?php
namespace Tests\Unit\Domain\Users\Actions;
use Tests\TestCase;
use App\Domain\Users\Actions\CreateUser;
use App\Domain\Users\DataTransferObjects\UserData;
use App\Domain\Users\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Hash;
class CreateUserTest extends TestCase
{
use RefreshDatabase;
/** @test */
public function it_creates_a_user_with_valid_data(): void
{
// Arrange
$userData = new UserData(
name: 'John Doe',
email: 'john@example.com',
password: 'Secret123!'
);
$action = new CreateUser();
// Act
$user = $action->execute($userData);
// Assert
$this->assertInstanceOf(User::class, $user);
$this->assertDatabaseHas('users', [
'email' => 'john@example.com',
'name' => 'John Doe',
]);
$this->assertTrue(Hash::check('Secret123!', $user->password));
$this->assertNotNull($user->email_verified_at);
}
/** @test */
public function it_throws_exception_for_duplicate_email(): void
{
// Arrange
User::factory()->create(['email' => 'existing@example.com']);
$userData = new UserData(
name: 'John Doe',
email: 'existing@example.com',
password: 'Secret123!'
);
$action = new CreateUser();
// Assert
$this->expectException(\Exception::class);
$this->expectExceptionMessage('Email already registered');
// Act
$action->execute($userData);
}
/** @test */
public function it_generates_uuid_for_new_user(): void
{
// Arrange
$userData = new UserData(
name: 'John Doe',
email: 'john@example.com',
password: 'Secret123!'
);
$action = new CreateUser();
// Act
$user = $action->execute($userData);
// Assert
$this->assertNotNull($user->uuid);
$this->assertMatchesRegularExpression(
'/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/',
$user->uuid
);
}
/** @test */
public function it_creates_user_profile_along_with_user(): void
{
// Arrange
$userData = new UserData(
name: 'John Doe',
email: 'john@example.com',
password: 'Secret123!'
);
$action = new CreateUser();
// Act
$user = $action->execute($userData);
// Assert
$this->assertNotNull($user->profile);
$this->assertEquals('New user', $user->profile->bio);
}
/** @test */
public function it_hashes_password_before_storing(): void
{
// Arrange
$userData = new UserData(
name: 'John Doe',
email: 'john@example.com',
password: 'plaintext-password'
);
$action = new CreateUser();
// Act
$user = $action->execute($userData);
// Assert
$this->assertNotEquals('plaintext-password', $user->password);
$this->assertTrue(Hash::check('plaintext-password', $user->password));
}
}
Feature Test Example:
<?php
namespace Tests\Feature\Api\V1;
use Tests\TestCase;
use App\Domain\Users\Models\User;
use Laravel\Sanctum\Sanctum;
use Illuminate\Foundation\Testing\RefreshDatabase;
class UserTest extends TestCase
{
use RefreshDatabase;
/** @test */
public function authenticated_user_can_get_their_profile(): void
{
// Arrange
$user = User::factory()->create();
Sanctum::actingAs($user, ['*']);
// Act
$response = $this->getJson('/api/v1/user');
// Assert
$response->assertOk()
->assertJsonStructure([
'data' => [
'id',
'name',
'email',
'created_at',
'updated_at',
]
])
->assertJson([
'data' => [
'email' => $user->email,
'name' => $user->name,
]
]);
}
/** @test */
public function unauthenticated_user_cannot_access_profile(): void
{
// Act
$response = $this->getJson('/api/v1/user');
// Assert
$response->assertUnauthorized();
}
/** @test */
public function user_can_update_their_profile(): void
{
// Arrange
$user = User::factory()->create();
Sanctum::actingAs($user, ['*']);
$updateData = [
'name' => 'Updated Name',
'email' => 'updated@example.com',
];
// Act
$response = $this->putJson('/api/v1/user', $updateData);
// Assert
$response->assertOk()
->assertJson([
'data' => [
'name' => 'Updated Name',
'email' => 'updated@example.com',
]
]);
$this->assertDatabaseHas('users', [
'id' => $user->id,
'name' => 'Updated Name',
'email' => 'updated@example.com',
]);
}
/** @test */
public function user_cannot_update_to_existing_email(): void
{
// Arrange
$user1 = User::factory()->create(['email' => 'user1@example.com']);
$user2 = User::factory()->create(['email' => 'user2@example.com']);
Sanctum::actingAs($user1, ['*']);
$updateData = [
'email' => 'user2@example.com', // Already taken
];
// Act
$response = $this->putJson('/api/v1/user', $updateData);
// Assert
$response->assertUnprocessable()
->assertJsonValidationErrors(['email']);
}
/** @test */
public function admin_can_view_all_users(): void
{
// Arrange
$admin = User::factory()->admin()->create();
$users = User::factory()->count(5)->create();
Sanctum::actingAs($admin, ['*']);
// Act
$response = $this->getJson('/api/v1/users');
// Assert
$response->assertOk()
->assertJsonStructure([
'data' => [
'*' => ['id', 'name', 'email', 'created_at']
],
'links',
'meta'
])
->assertJsonCount(6, 'data'); // Admin + 5 users
}
/** @test */
public function non_admin_cannot_view_all_users(): void
{
// Arrange
$user = User::factory()->create();
Sanctum::actingAs($user, ['*']);
// Act
$response = $this->getJson('/api/v1/users');
// Assert
$response->assertForbidden();
}
/** @test */
public function it_validates_user_update_data(): void
{
// Arrange
$user = User::factory()->create();
Sanctum::actingAs($user, ['*']);
$invalidData = [
'name' => '', // Empty name
'email' => 'invalid-email', // Invalid email
];
// Act
$response = $this->putJson('/api/v1/user', $invalidData);
// Assert
$response->assertUnprocessable()
->assertJsonValidationErrors(['name', 'email']);
}
/** @test */
public function user_can_delete_their_account(): void
{
// Arrange
$user = User::factory()->create();
Sanctum::actingAs($user, ['*']);
// Act
$response = $this->deleteJson('/api/v1/user');
// Assert
$response->assertNoContent();
$this->assertSoftDeleted('users', ['id' => $user->id]);
}
}
Query Optimization Techniques:
// 1. Select only needed columns
// ❌ BAD: Selects all columns
$users = User::all();
// ✅ GOOD: Select only needed columns
$users = User::select(['id', 'name', 'email'])->get();
// 2. Use chunking for large datasets
// ❌ BAD: Loads all records into memory
User::all()->each(function ($user) {
processUser($user);
});
// ✅ GOOD: Processes in chunks
User::chunk(1000, function ($users) {
foreach ($users as $user) {
processUser($user);
}
});
// ✅ BETTER: Use cursor for very large datasets
foreach (User::cursor() as $user) {
processUser($user);
}
// 3. Efficient relationship loading
// ❌ BAD: Multiple queries
$posts = Post::all();
foreach ($posts as $post) {
$comments = $post->comments; // N+1 problem
}
// ✅ GOOD: Eager loading with constraints
$posts = Post::with([
'comments' => function ($query) {
$query->select('id', 'post_id', 'content')
->where('approved', true)
->orderBy('created_at', 'desc')
->limit(5);
},
'comments.user:id,name,avatar',
])->get();
// 4. Use database indexes effectively
class CreatePostsTable extends Migration
{
public function up()
{
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->text('content');
$table->foreignId('user_id')->constrained();
$table->boolean('published')->default(false);
$table->timestamp('published_at')->nullable();
$table->timestamps();
$table->softDeletes();
// Add indexes for common queries
$table->index(['published', 'published_at']);
$table->index(['user_id', 'created_at']);
$table->fullText(['title', 'content']);
// Composite index for specific queries
$table->index(['published', 'user_id', 'created_at']);
});
}
}
// 5. Use subqueries for aggregates
$users = User::addSelect([
'posts_count' => Post::selectRaw('COUNT(*)')
->whereColumn('user_id', 'users.id'),
'last_post_date' => Post::select('created_at')
->whereColumn('user_id', 'users.id')
->latest()
->limit(1),
])->get();
// 6. Cache expensive queries
public function getPopularPosts()
{
return Cache::remember('popular_posts', 3600, function () {
return Post::withCount('likes')
->with(['user:id,name', 'category:id,name'])
->where('published', true)
->where('created_at', '>', now()->subMonth())
->orderByDesc('likes_count')
->limit(10)
->get();
});
}
// 7. Use database views for complex queries
DB::statement('
CREATE VIEW user_stats AS
SELECT
u.id,
u.name,
u.email,
COUNT(DISTINCT p.id) as post_count,
COUNT(DISTINCT c.id) as comment_count,
COALESCE(SUM(p.views), 0) as total_views,
MAX(p.created_at) as last_post_date
FROM users u
LEFT JOIN posts p ON p.user_id = u.id AND p.published = true
LEFT JOIN comments c ON c.user_id = u.id AND c.approved = true
GROUP BY u.id, u.name, u.email
');
// Usage
$stats = DB::table('user_stats')->get();
Security Middleware Stack:
<?php
namespace App\Infrastructure\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class SecurityHeaders
{
public function handle(Request $request, Closure $next): Response
{
$response = $next($request);
// Security headers
$response->headers->set('X-Content-Type-Options', 'nosniff');
$response->headers->set('X-Frame-Options', 'SAMEORIGIN');
$response->headers->set('X-XSS-Protection', '1; mode=block');
$response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin');
$response->headers->set('Permissions-Policy', 'geolocation=(), microphone=()');
// Content Security Policy
$csp = $this->buildCspHeader();
$response->headers->set('Content-Security-Policy', $csp);
// Strict Transport Security (HTTPS only)
if ($request->secure()) {
$response->headers->set(
'Strict-Transport-Security',
'max-age=31536000; includeSubDomains; preload'
);
}
return $response;
}
private function buildCspHeader(): string
{
$directives = [
"default-src 'self'",
"script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://www.google-analytics.com",
"style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net",
"img-src 'self' data: https: blob:",
"font-src 'self' https://cdn.jsdelivr.net",
"connect-src 'self' https://api.example.com wss://ws.example.com",
"frame-src 'self' https://www.youtube.com",
"object-src 'none'",
"media-src 'self'",
"form-action 'self'",
"frame-ancestors 'self'",
"base-uri 'self'",
"manifest-src 'self'",
];
return implode('; ', $directives);
}
}
GitHub Actions Workflow:
name: Laravel CI/CD Pipeline
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
analyze:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
extensions: mbstring, xml, ctype, iconv, intl, pdo_mysql, bcmath, redis, gd, zip
coverage: xdebug
- name: Install Dependencies
run: |
composer install --prefer-dist --no-progress --no-interaction
npm ci
- name: Copy Environment
run: cp .env.example .env
- name: Generate Key
run: php artisan key:generate
- name: PHP Syntax Check
run: find . -type f -name "*.php" -exec php -l {} \;
- name: PHPStan Analysis
run: vendor/bin/phpstan analyse --memory-limit=2G
- name: Laravel Pint
run: vendor/bin/pint --test
- name: Security Check
run: vendor/bin/security-checker security:check
- name: Build Assets
run: npm run build
test:
runs-on: ubuntu-latest
needs: analyze
services:
mysql:
image: mysql:8.0
env:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: laravel_test
options: >-
--health-cmd="mysqladmin ping"
--health-interval=10s
--health-timeout=5s
--health-retries=3
ports:
- 3306:3306
redis:
image: redis:alpine
options: >-
--health-cmd="redis-cli ping"
--health-interval=10s
--health-timeout=5s
--health-retries=3
ports:
- 6379:6379
steps:
- uses: actions/checkout@v3
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
extensions: mbstring, xml, ctype, iconv, intl, pdo_mysql, bcmath, redis, gd, zip
coverage: xdebug
- name: Install Dependencies
run: composer install --prefer-dist --no-progress --no-interaction
- name: Prepare Environment
run: |
cp .env.example .env.test
echo "APP_ENV=testing" >> .env.test
echo "DB_CONNECTION=mysql" >> .env.test
echo "DB_HOST=127.0.0.1" >> .env.test
echo "DB_PORT=3306" >> .env.test
echo "DB_DATABASE=laravel_test" >> .env.test
echo "DB_USERNAME=root" >> .env.test
echo "DB_PASSWORD=root" >> .env.test
echo "REDIS_HOST=127.0.0.1" >> .env.test
echo "REDIS_PORT=6379" >> .env.test
- name: Generate Key
run: php artisan key:generate --env=testing
- name: Create Database
run: |
mysql -h 127.0.0.1 -u root -proot -e "CREATE DATABASE IF NOT EXISTS laravel_test"
mysql -h 127.0.0.1 -u root -proot -e "ALTER DATABASE laravel_test CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci"
- name: Run Migrations
run: php artisan migrate --env=testing --force
- name: Run Tests
run: |
php artisan test --parallel --reports=dots
env:
DB_CONNECTION: mysql
DB_HOST: 127.0.0.1
DB_PORT: 3306
DB_DATABASE: laravel_test
DB_USERNAME: root
DB_PASSWORD: root
- name: Upload Coverage
uses: codecov/codecov-action@v3
with:
file: ./coverage.xml
flags: unittests
deploy-staging:
runs-on: ubuntu-latest
needs: test
if: github.ref == 'refs/heads/develop'
environment: staging
steps:
- uses: actions/checkout@v3
- name: Setup SSH
uses: webfactory/ssh-agent@v0.5.3
with:
ssh-private-key: ${{ secrets.STAGING_SSH_KEY }}
- name: Deploy to Staging
run: |
ssh -o StrictHostKeyChecking=no ${{ secrets.STAGING_USER }}@${{ secrets.STAGING_HOST }} << 'EOF'
cd /var/www/staging
git pull origin develop
composer install --no-dev --optimize-autoloader
npm ci --only=production
npm run build
php artisan migrate --force
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan queue:restart
sudo systemctl reload php8.2-fpm
EOF
deploy-production:
runs-on: ubuntu-latest
needs: test
if: github.ref == 'refs/heads/main'
environment: production
steps:
- uses: actions/checkout@v3
- name: Setup SSH
uses: webfactory/ssh-agent@v0.5.3
with:
ssh-private-key: ${{ secrets.PRODUCTION_SSH_KEY }}
- name: Deploy to Production
run: |
ssh -o StrictHostKeyChecking=no ${{ secrets.PRODUCTION_USER }}@${{ secrets.PRODUCTION_HOST }} << 'EOF'
cd /var/www/production
# Enable maintenance mode
php artisan down --secret=${{ secrets.MAINTENANCE_SECRET }}
# Backup database
mysqldump -u${{ secrets.DB_USERNAME }} -p${{ secrets.DB_PASSWORD }} ${{ secrets.DB_DATABASE }} > backup-$(date +%Y%m%d-%H%M%S).sql
# Deploy new code
git fetch origin main
git reset --hard origin/main
# Install dependencies
composer install --no-dev --optimize-autoloader --no-interaction
npm ci --only=production --silent
npm run build --silent
# Run migrations
php artisan migrate --force
# Clear and cache config
php artisan config:clear
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan event:cache
# Set permissions
sudo chown -R www-data:www-data storage bootstrap/cache
sudo chmod -R 775 storage bootstrap/cache
# Restart services
php artisan queue:restart
sudo supervisorctl restart all
sudo systemctl reload php8.2-fpm
sudo systemctl reload nginx
# Clear opcache
sudo service php8.2-fpm reload
# Disable maintenance mode
php artisan up
# Health check
curl -f https://${{ secrets.PRODUCTION_DOMAIN }}/health || exit 1
EOF
- name: Notify Deployment
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
channel: '#deployments'
author_name: GitHub Actions
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
PHPStan Configuration:
# phpstan.neon
parameters:
level: 8
paths:
- app
- database/factories
- database/seeders
- tests
excludePaths:
- app/Infrastructure/Http/Middleware/TrustProxies.php
- app/Infrastructure/Exceptions/Handler.php
fileExtensions:
- php
parallel:
maximumNumberOfProcesses: 4
memoryLimit: 2G
# Laravel-specific configuration
checkMissingIterableValueType: false
# Ignore certain error types
ignoreErrors:
- '#Call to an undefined method Illuminate\\Database\\Eloquent\\Builder::#'
- '#Access to an undefined property Illuminate\\Support\\Facades\\Auth::#'
# Custom rules
rules:
- PHPStan\Rules\Functions\CallToFunctionStatementWithoutSideEffectsRule
# Bleeding edge for PHP 8.2 features
featureToggles:
readonlyClasses: true
readonlyProperties: true
# Dynamic return type extensions for Laravel
dynamicConstantNames:
- config
- env
# Custom early terminating method calls
earlyTerminatingMethodCalls:
App\Domain\Users\Models\User:
- abortIf
- abortUnless
# Report unmatched ignored errors
reportUnmatchedIgnoredErrors: false
includes:
- vendor/nunomaduro/larastan/extension.neon
- vendor/phpstan/phpstan-deprecation-rules/rules.neon
- vendor/phpstan/phpstan-phpunit/rules.neon
- vendor/phpstan/phpstan-strict-rules/rules.neon
Writing clean and maintainable Laravel code is both an art and a science. By following these best practices, you'll create applications that are:
Key Takeaways:
Continuous Improvement Checklist:
Remember, clean code is not a destination but a journey. Start implementing these practices today, and continually refine your approach as you gain more experience.
Next Steps: