Skip to content
ExponaBLOG
PHP & LARAVEL DEVELOPMENT · DAY 100 / 100

100 days of PHP and Laravel: the recap, explained simply

The lessons that matter most.

Expona Admin6 min read

100 days of PHP and Laravel: the recap, explained simply

Over the past hundred days, we've built real applications, debugged production issues, and learned what separates solid Laravel codebases from fragile ones. This isn't a marketing recap—it's what actually matters when you're shipping features every sprint and fixing bugs at 2am. The lessons that matter most centre on four pillars: modern PHP fundamentals, framework mastery, database discipline, and production readiness.

Modern PHP is typed and fast — use strict types, enums and static analysis

PHP 8.1 and beyond have transformed the language. Developers who still write loose, untyped code are leaving performance on the table and creating bugs that static analysis could catch in seconds. Strict types aren't optional anymore—they're the baseline for professional work.

When you declare declare(strict_types=1); at the top of a file, PHP enforces type hints at runtime. A parameter typed as int won't silently accept a string. This catches integration bugs early, before they reach staging or production. Combined with tools like PHPStan or Psalm, you can identify entire classes of errors without running a single test.

Enums, introduced in PHP 8.1, let you model domain concepts precisely. Instead of magic strings like 'pending', 'approved', 'rejected' scattered through your codebase, you define:

php
enum OrderStatus: string {
    case Pending = 'pending';
    case Approved = 'approved';
    case Rejected = 'rejected';
}

Now your database queries, API responses, and business logic all use the same type. IDEs autocomplete. Refactoring is safe. Serialization is explicit. You can add methods to enums too:

php
enum OrderStatus: string {
    case Pending = 'pending';
    case Approved = 'approved';
    case Rejected = 'rejected';

    public function isTerminal(): bool {
        return $this === self::Approved || $this === self::Rejected;
    }
}

Run PHPStan at level 9 in your CI pipeline. If you're not doing this, you're debugging at runtime what the static analyzer could have caught. The time you save compounds across thousands of deployments.

Laravel gives you a lot — auth, queues, mail and scheduling — learn them before adding packages

Laravel's built-in features are mature and battle-tested. Yet many teams rush to add third-party packages for authentication, background jobs, or email without understanding what the framework already provides. This creates technical debt: more dependencies, more version conflicts, more surface area for bugs.

Take authentication. Laravel's Auth facade and Gate/Policy system handle 90% of real-world scenarios. Before you pull in a separate authorization package, build your policies:

php
class InvoicePolicy {
    public function update(User $user, Invoice $invoice): bool {
        return $user->id === $invoice->user_id 
            && $invoice->status !== InvoiceStatus::Paid;
    }
}

Queues are similar. Laravel's queue system with Redis or database drivers handles async jobs, retries, rate limiting, and monitoring. The middleware, job batches, and async execution model are production-ready. Most of us don't need a separate message queue framework—we need discipline in how we structure jobs.

Scheduling—via app/Console/Kernel.php—lets you define recurring tasks in code. Reports, cleanup, sync operations. It's easy to understand, version-controlled, and doesn't require separate tools.

Mail is the same pattern. Laravel abstracts multiple drivers (Mailgun, SendGrid, SES, SMTP) behind a consistent interface. Before adding a specialized mail service, leverage what's there.

The common thread: learn the framework thoroughly. Read the documentation. Build small features with the native tools. Only when you hit a genuine limitation do you add a specialized package. This approach keeps your codebase shallow and teams productive.

Eloquent needs discipline — eager loading, scopes and transactions keep apps fast and correct

Eloquent ORM is expressive and convenient. It's also easy to write code that works in development but crawls in production. The N+1 query problem is the most obvious trap, but there are deeper patterns that separate slow apps from fast ones.

Eager loading must become reflex. If you're looping over users and accessing their invoices, you must call with('invoices') on the query:

php
// Bad: 1 + N queries
$users = User::all();
foreach ($users as $user) {
    echo $user->invoices->count();
}

// Good: 2 queries
$users = User::with('invoices')->get();
foreach ($users as $user) {
    echo $user->invoices->count();
}

Use query scopes to encapsulate common filters. Rather than repeating where('status', 'active') across your codebase, define it once:

php
class User extends Model {
    public function scopeActive($query) {
        return $query->where('status', 'active');
    }
}

// Usage
User::active()->with('invoices')->get();

Transactions protect data consistency. If you're updating multiple tables as part of a single business operation—like marking an order paid and incrementing account balance—wrap it in a transaction. If the second update fails, the first rolls back:

php
DB::transaction(function () {
    $order->update(['status' => OrderStatus::Paid]);
    $order->user->increment('balance', $order->total);
});

Without this, you can end up with paid orders that didn't credit the user. In production, that's a support nightmare and a revenue leak.

Production matters — caching, queues, monitoring and safe deploys

Development and production are different environments. Code that works locally with one user and a small dataset will fail under load with thousands of concurrent requests. The gap between "working" and "production-ready" is caching, async work, visibility, and safe deployment practices.

Caching isn't optional. Database queries for frequently accessed data—user permissions, feature flags, product catalogs—should hit Redis or Memcached, not hit the database every request. Laravel's cache facade makes this straightforward:

php
$permissions = cache()->remember(
    "user.{$user->id}.permissions",
    now()->addHours(24),
    fn () => $user->permissions()->pluck('name')->toArray()
);

Queues move slow operations off the request cycle. Sending emails, processing images, syncing external APIs—these belong in background jobs, not in HTTP handlers. If you don't queue them, users wait. If the job fails, the entire request fails. Async work decouples concerns and keeps your app responsive.

Monitoring tells you what's actually happening. Application performance monitoring tools (APM), error tracking, log aggregation, and basic metrics (response time, error rate, queue depth) are non-negotiable. You can't optimize what you don't measure. You can't debug production issues without visibility into what's happening.

Safe deploys prevent outages. Zero-downtime deployments with health checks, feature flags for incomplete features, and database migrations that run independently of code changes are table stakes. Rolling out code should not cause downtime or leave your database in a bad state.

Common mistakes

  • Skipping static analysis because "tests pass locally". PHPStan catches bugs tests don't. Run it in CI.
  • Lazy-loading relationships in loops. You'll spot it in staging but not until you've shipped to production and users are affected.
  • Treating caching as optional. Caching is foundational to performance. Plan for it from day one, not after profiling shows bottlenecks.
  • Deploying without monitoring. You can't know if your deploy broke things if you're not watching.
  • Not using database transactions for multi-step operations. Data consistency is non-negotiable. Wrap risky operations in transactions.

Key takeaways

  • Use strict types and static analysis. They catch bugs at dev time, not production time.
  • Master Laravel's built-in features before reaching for specialized packages. Auth, queues, mail, and scheduling are production-ready.
  • Eager load relationships, use scopes, and wrap multi-step operations in transactions. Eloquent is powerful but requires discipline.
  • Caching, queues, monitoring, and safe deploys aren't "nice to have"—they're foundational to production systems.
  • The difference between "working" code and "production-ready" code is discipline in these areas.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles