API 2 ‐ Laravel ‐ CRUD Operation - johnverz22/appdev-lessons GitHub Wiki

Laravel Backend Development Guide

1. Introduction to Laravel

1.1 What is Laravel?

Laravel is a free, open-source PHP web framework created by Taylor Otwell in 2011. It follows the model-view-controller (MVC) architectural pattern and is designed to make the development process more enjoyable by simplifying common tasks used in many web projects.

Key features:

  • Elegant syntax and modern tooling
  • Robust dependency management through Composer
  • Powerful database abstraction layer (Eloquent ORM)
  • Built-in security features
  • Extensive ecosystem and community support

1.2 Setting up Laravel

Prerequisites

Before installing Laravel, ensure you have:

  • PHP >= 8.1
  • Composer (Dependency Manager for PHP)
  • Node.js & NPM (for frontend assets)
  • A database system (MySQL, PostgreSQL, SQLite, or SQL Server)

Installation Steps

  1. Install Composer

    # Windows: Download and run Composer-Setup.exe from https://getcomposer.org/
    # Mac/Linux:
    curl -sS https://getcomposer.org/installer | php
    mv composer.phar /usr/local/bin/composer
    
  2. Create a New Laravel Project

    composer create-project laravel/laravel example-app
    cd example-app
    
  3. Configure Environment

    • Copy .env.example to .env
    • Generate application key:
      php artisan key:generate
      
    • Update database credentials in .env

1.3 Laravel Directory Structure

laravel-project/
├── app/                # Core application code
│   ├── Http/           # Controllers, Middleware, Requests
│   ├── Models/         # Eloquent model classes
│   └── Providers/      # Service providers
├── config/             # Configuration files
├── database/           # Migrations, seeds, factories
├── public/             # Web server entry point
├── resources/          # Views, raw assets
├── routes/             # Route definitions
│   ├── api.php         # API routes
│   └── web.php         # Web routes
├── storage/            # Logs, cache, compiled files
└── tests/              # Automated tests

1.4 Running the Development Server

php artisan serve

This will start a development server at http://localhost:8000

2. Basics of Routing and Controllers

2.1 Understanding Routes

Routes in Laravel define how your application responds to HTTP requests. They are defined in files within the routes directory.

Types of Route Files

  • routes/web.php: For web routes (pages, forms)
  • routes/api.php: For API endpoints (automatically prefixed with '/api')

Basic Route Examples

// routes/web.php
Route::get('/hello', function () {
    return 'Hello World';
});

// routes/api.php
Route::get('/users', function () {
    return ['user1', 'user2'];
});

Route Parameters

Route::get('/users/{id}', function ($id) {
    return 'User ' . $id;
});

// Optional parameters
Route::get('/posts/{post?}', function ($post = null) {
    return $post ?? 'All posts';
});

2.2 Controllers

Controllers organize related request handling logic into classes.

Creating a Controller

php artisan make:controller UserController

This creates app/Http/Controllers/UserController.php

Basic Controller Structure

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class UserController extends Controller
{
    public function index()
    {
        return ['message' => 'List of users'];
    }

    public function show($id)
    {
        return ['message' => "User $id details"];
    }
}

Connecting Routes to Controllers

// routes/api.php
use App\Http\Controllers\UserController;

Route::get('/users', [UserController::class, 'index']);
Route::get('/users/{id}', [UserController::class, 'show']);

2.3 Resource Controllers

For CRUD operations, Laravel provides resource controllers:

php artisan make:controller ProductController --resource

This creates a controller with these methods:

  • index() - List resources
  • create() - Show create form
  • store() - Save new resource
  • show() - Show single resource
  • edit() - Show edit form
  • update() - Update resource
  • destroy() - Delete resource

Route Registration

Route::resource('products', ProductController::class);

Best Practices

  1. Keep controllers focused and lean
  2. Use meaningful names for routes and controllers
  3. Group related routes using route prefixes and middleware
  4. Use route names for easier reference
  5. Validate incoming requests
  6. Return consistent JSON responses for APIs

3. Working with Models and Migrations

3.1 Eloquent Models

Eloquent is Laravel's built-in ORM (Object-Relational Mapper) that provides a beautiful, simple ActiveRecord implementation for working with your database.

Creating a Model

# Create a model
php artisan make:model Product

# Create a model with migration
php artisan make:model Product -m

# Create a model with migration, factory, and controller
php artisan make:model Product -mfc

Basic Model Structure

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Product extends Model
{
    // Specify which fields can be mass-assigned
    protected $fillable = [
        'name',
        'description',
        'price'
    ];

    // Or specify which fields cannot be mass-assigned
    protected $guarded = ['id'];

    // Customize table name (if different from convention)
    protected $table = 'products';

    // Disable timestamps if you don't want created_at and updated_at
    public $timestamps = false;
}

3.2 Migrations

Migrations are like version control for your database, allowing your team to modify and share the database schema.

Creating a Migration

php artisan make:migration create_products_table

Basic Migration Structure

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateProductsTable extends Migration
{
    public function up()
    {
        Schema::create('products', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->text('description')->nullable();
            $table->decimal('price', 8, 2);
            $table->timestamps();
        });
    }

    public function down()
    {
        Schema::dropIfExists('products');
    }
}

Common Column Types

$table->string('column');           // VARCHAR
$table->text('column');            // TEXT
$table->integer('column');         // INTEGER
$table->decimal('column', 8, 2);   // DECIMAL
$table->boolean('column');         // BOOLEAN
$table->date('column');            // DATE
$table->datetime('column');        // DATETIME
$table->timestamp('column');       // TIMESTAMP

Running Migrations

# Run all pending migrations
php artisan migrate

# Rollback last migration batch
php artisan migrate:rollback

# Rollback all migrations and run them again
php artisan migrate:fresh

3.3 Model Relationships

// One-to-Many Relationship Example
class User extends Model
{
    public function orders()
    {
        return $this->hasMany(Order::class);
    }
}

class Order extends Model
{
    public function user()
    {
        return $this->belongsTo(User::class);
    }
}

4. CRUD Operations

4.1 Creating a Resource Controller

php artisan make:controller ProductController --resource

4.2 Complete CRUD Implementation

namespace App\Http\Controllers;

use App\Models\Product;
use Illuminate\Http\Request;

class ProductController extends Controller
{
    /**
     * Display a listing of products
     */
    public function index()
    {
        $products = Product::all();
        return response()->json($products);
    }

    /**
     * Store a new product
     */
    public function store(Request $request)
    {
        $validated = $request->validate([
            'name' => 'required|max:255',
            'description' => 'nullable',
            'price' => 'required|numeric|min:0'
        ]);

        $product = Product::create($validated);
        return response()->json($product, 201);
    }

    /**
     * Display a specific product
     */
    public function show(Product $product)
    {
        return response()->json($product);
    }

    /**
     * Update a specific product
     */
    public function update(Request $request, Product $product)
    {
        $validated = $request->validate([
            'name' => 'sometimes|required|max:255',
            'description' => 'nullable',
            'price' => 'sometimes|required|numeric|min:0'
        ]);

        $product->update($validated);
        return response()->json($product);
    }

    /**
     * Delete a specific product
     */
    public function destroy(Product $product)
    {
        $product->delete();
        return response()->json(null, 204);
    }
}

4.3 Route Registration

// routes/api.php
use App\Http\Controllers\ProductController;

Route::apiResource('products', ProductController::class);

4.4 Using Eloquent for Database Operations

Basic Operations

// Create
$product = Product::create([
    'name' => 'New Product',
    'price' => 99.99
]);

// Read
$product = Product::find(1);
$products = Product::where('price', '>', 100)->get();

// Update
$product->update(['price' => 79.99]);

// Delete
$product->delete();

Advanced Queries

// Filtering
$products = Product::where('price', '>', 100)
    ->where('name', 'like', '%phone%')
    ->get();

// Ordering
$products = Product::orderBy('price', 'desc')->get();

// Pagination
$products = Product::paginate(20);

// Eager Loading Relationships
$orders = Order::with('user')->get();

4.5 Best Practices for CRUD Operations

  1. Input Validation

    • Always validate incoming requests
    • Use Laravel's built-in validation system
    • Create form requests for complex validation
  2. Response Consistency

    • Use appropriate HTTP status codes
    • Maintain consistent response structure
    • Include meaningful error messages
  3. Database Operations

    • Use mass assignment protection ($fillable or $guarded)
    • Implement soft deletes for recoverable data
    • Use database transactions for complex operations
  4. Security

    • Implement proper authentication/authorization
    • Protect against CSRF attacks
    • Sanitize user input
  5. Performance

    • Use eager loading to avoid N+1 queries
    • Implement caching when appropriate
    • Paginate large result sets

5. Working with Foreign Keys

5.1 Understanding Foreign Keys

Foreign keys are database constraints that establish relationships between tables. They ensure referential integrity and help maintain data consistency.

5.2 Creating Tables with Foreign Keys

// Create users table migration
class CreateUsersTable extends Migration
{
    public function up()
    {
        Schema::create('users', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('email')->unique();
            $table->timestamps();
        });
    }
}

// Create posts table with foreign key
class CreatePostsTable extends Migration
{
    public function up()
    {
        Schema::create('posts', function (Blueprint $table) {
            $table->id();
            $table->string('title');
            $table->text('content');
            // Define foreign key
            $table->foreignId('user_id')
                  ->constrained()
                  ->onDelete('cascade');
            $table->timestamps();
        });
    }
}

5.3 Foreign Key Options

// Different ways to define foreign keys
$table->foreignId('user_id')
      ->constrained();  // Automatically determines table and column names

// Specify custom table name
$table->foreignId('user_id')
      ->constrained('users');

// Custom column reference
$table->foreignId('author_id')
      ->constrained('users', 'id');

// Define onDelete and onUpdate actions
$table->foreignId('user_id')
      ->constrained()
      ->onDelete('cascade')  // Also: 'restrict', 'no action', 'set null'
      ->onUpdate('cascade');

5.4 Defining Model Relationships

// User Model
class User extends Model
{
    public function posts()
    {
        return $this->hasMany(Post::class);
    }
}

// Post Model
class Post extends Model
{
    public function user()
    {
        return $this->belongsTo(User::class);
    }
}

5.5 Database Seeding

Creating Factories

// PostFactory.php
namespace Database\Factories;

use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;

class PostFactory extends Factory
{
    public function definition()
    {
        return [
            'title' => $this->faker->sentence,
            'content' => $this->faker->paragraphs(3, true),
            'user_id' => User::factory()
        ];
    }
}

Creating Seeders

// DatabaseSeeder.php
namespace Database\Seeders;

use Illuminate\Database\Seeder;
use App\Models\User;
use App\Models\Post;

class DatabaseSeeder extends Seeder
{
    public function run()
    {
        User::factory()
            ->count(10)
            ->has(Post::factory()->count(3))
            ->create();
    }
}

6. Introduction to Middleware

6.1 What is Middleware?

Middleware provides a convenient mechanism for filtering HTTP requests entering your application. It can:

  • Perform authentication
  • Verify CSRF tokens
  • Check rate limits
  • Modify request/response
  • And more...

6.2 Using Built-in Middleware

// routes/api.php
Route::middleware(['auth:sanctum'])->group(function () {
    Route::apiResource('posts', PostController::class);
});

// Using multiple middleware
Route::middleware(['auth:sanctum', 'throttle:60,1'])->group(function () {
    Route::get('/sensitive-data', [DataController::class, 'index']);
});

6.3 Creating Custom Middleware

Generate Middleware

php artisan make:middleware CheckUserRole

Implement Middleware Logic

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;

class CheckUserRole
{
    public function handle(Request $request, Closure $next, $role)
    {
        if (!$request->user() || $request->user()->role !== $role) {
            return response()->json(['error' => 'Unauthorized'], 403);
        }

        return $next($request);
    }
}

6.4 Registering Middleware

// app/Http/Kernel.php
protected $routeMiddleware = [
    // ... other middleware
    'check.role' => \App\Http\Middleware\CheckUserRole::class,
];

6.5 Applying Custom Middleware

// Single route
Route::get('/admin/dashboard', [AdminController::class, 'index'])
    ->middleware('check.role:admin');

// Group of routes
Route::middleware(['check.role:editor'])->group(function () {
    Route::resource('articles', ArticleController::class);
});

6.6 Common Middleware Use Cases

Rate Limiting

// Limit requests to 60 per minute
Route::middleware(['throttle:60,1'])->group(function () {
    Route::apiResource('posts', PostController::class);
});

CORS Middleware

// config/cors.php
return [
    'paths' => ['api/*'],
    'allowed_methods' => ['*'],
    'allowed_origins' => ['*'],
    'allowed_origins_patterns' => [],
    'allowed_headers' => ['*'],
    'exposed_headers' => [],
    'max_age' => 0,
    'supports_credentials' => false,
];

Authentication Check

Route::middleware(['auth:sanctum'])->group(function () {
    Route::get('/user/profile', function (Request $request) {
        return $request->user();
    });
});

6.7 Best Practices for Middleware

  1. Keep It Simple

    • Each middleware should have a single responsibility
    • Avoid complex logic in middleware
    • Use multiple middleware instead of one complex one
  2. Performance Considerations

    • Be mindful of middleware that runs on every request
    • Cache expensive operations
    • Use middleware groups wisely
  3. Error Handling

    • Return appropriate status codes
    • Provide clear error messages
    • Log middleware failures when appropriate
  4. Security

    • Validate input in middleware when necessary
    • Implement proper error handling
    • Be careful with sensitive data

7. Authentication with Laravel Sanctum

7.1 Setting Up Sanctum

Installation

composer require laravel/sanctum

# Publish configuration and migration files
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"

# Run migrations
php artisan migrate

Configuration

// config/sanctum.php
return [
    'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf(
        '%s%s',
        'localhost,localhost:3000,localhost:8080,127.0.0.1,127.0.0.1:8000,::1',
        env('APP_URL') ? ','.parse_url(env('APP_URL'), PHP_URL_HOST) : ''
    ))),
    'expiration' => null,
    'middleware' => [
        'verify_csrf_token' => App\Http\Middleware\VerifyCsrfToken::class,
        'encrypt_cookies' => App\Http\Middleware\EncryptCookies::class,
    ],
];

7.2 User Authentication Implementation

User Registration Controller

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;

class RegisterController extends Controller
{
    public function register(Request $request)
    {
        $validated = $request->validate([
            'name' => 'required|string|max:255',
            'email' => 'required|string|email|unique:users|max:255',
            'password' => 'required|string|min:8|confirmed'
        ]);

        $user = User::create([
            'name' => $validated['name'],
            'email' => $validated['email'],
            'password' => Hash::make($validated['password'])
        ]);

        $token = $user->createToken('auth_token')->plainTextToken;

        return response()->json([
            'user' => $user,
            'access_token' => $token,
            'token_type' => 'Bearer'
        ], 201);
    }
}

Login Controller

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

class LoginController extends Controller
{
    public function login(Request $request)
    {
        $validated = $request->validate([
            'email' => 'required|email',
            'password' => 'required'
        ]);

        if (!Auth::attempt($validated)) {
            return response()->json([
                'message' => 'Invalid login credentials'
            ], 401);
        }

        $user = Auth::user();
        $token = $user->createToken('auth_token')->plainTextToken;

        return response()->json([
            'user' => $user,
            'access_token' => $token,
            'token_type' => 'Bearer'
        ]);
    }

    public function logout(Request $request)
    {
        $request->user()->currentAccessToken()->delete();

        return response()->json([
            'message' => 'Successfully logged out'
        ]);
    }
}

7.3 Authentication Routes

// routes/api.php
use App\Http\Controllers\Auth\RegisterController;
use App\Http\Controllers\Auth\LoginController;

Route::post('/register', [RegisterController::class, 'register']);
Route::post('/login', [LoginController::class, 'login']);

// Protected routes
Route::middleware('auth:sanctum')->group(function () {
    Route::post('/logout', [LoginController::class, 'logout']);
    Route::get('/user', function (Request $request) {
        return $request->user();
    });
});

8. Building Protected CRUD Endpoints

8.1 Protected Resource Controller

namespace App\Http\Controllers;

use App\Models\Post;
use Illuminate\Http\Request;

class PostController extends Controller
{
    public function __construct()
    {
        $this->middleware('auth:sanctum');
    }

    public function index()
    {
        $posts = Post::with('user')
            ->where('user_id', auth()->id())
            ->latest()
            ->paginate(10);

        return response()->json($posts);
    }

    public function store(Request $request)
    {
        $validated = $request->validate([
            'title' => 'required|string|max:255',
            'content' => 'required|string'
        ]);

        $post = auth()->user()->posts()->create($validated);

        return response()->json($post, 201);
    }

    public function show(Post $post)
    {
        $this->authorize('view', $post);
        
        return response()->json($post->load('user'));
    }

    public function update(Request $request, Post $post)
    {
        $this->authorize('update', $post);

        $validated = $request->validate([
            'title' => 'sometimes|required|string|max:255',
            'content' => 'sometimes|required|string'
        ]);

        $post->update($validated);

        return response()->json($post);
    }

    public function destroy(Post $post)
    {
        $this->authorize('delete', $post);

        $post->delete();

        return response()->json(null, 204);
    }
}

8.2 Policy for Authorization

namespace App\Policies;

use App\Models\Post;
use App\Models\User;

class PostPolicy
{
    public function view(User $user, Post $post)
    {
        return $user->id === $post->user_id;
    }

    public function update(User $user, Post $post)
    {
        return $user->id === $post->user_id;
    }

    public function delete(User $user, Post $post)
    {
        return $user->id === $post->user_id;
    }
}

8.3 Protected Routes Configuration

// routes/api.php
Route::middleware('auth:sanctum')->group(function () {
    Route::apiResource('posts', PostController::class);
});

8.4 Error Handling

namespace App\Exceptions;

use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Auth\Access\AuthorizationException;
use Throwable;

class Handler extends ExceptionHandler
{
    public function render($request, Throwable $exception)
    {
        if ($exception instanceof AuthenticationException) {
            return response()->json([
                'message' => 'Unauthenticated.'
            ], 401);
        }

        if ($exception instanceof AuthorizationException) {
            return response()->json([
                'message' => 'This action is unauthorized.'
            ], 403);
        }

        return parent::render($request, $exception);
    }
}

8.5 Testing Protected Endpoints

namespace Tests\Feature;

use App\Models\User;
use App\Models\Post;
use Tests\TestCase;
use Laravel\Sanctum\Sanctum;

class PostControllerTest extends TestCase
{
    public function test_user_can_create_post()
    {
        $user = User::factory()->create();
        Sanctum::actingAs($user);

        $response = $this->postJson('/api/posts', [
            'title' => 'Test Post',
            'content' => 'Test Content'
        ]);

        $response->assertStatus(201)
                ->assertJsonStructure([
                    'id',
                    'title',
                    'content',
                    'user_id',
                    'created_at',
                    'updated_at'
                ]);
    }

    public function test_user_cannot_update_others_post()
    {
        $user = User::factory()->create();
        $otherUser = User::factory()->create();
        $post = Post::factory()->create(['user_id' => $otherUser->id]);

        Sanctum::actingAs($user);

        $response = $this->putJson("/api/posts/{$post->id}", [
            'title' => 'Updated Title'
        ]);

        $response->assertStatus(403);
    }
}

8.6 Best Practices for Protected Endpoints

  1. Security

    • Always validate user authentication
    • Implement proper authorization using policies
    • Use HTTPS in production
    • Implement rate limiting
  2. Token Management

    • Implement token expiration
    • Allow users to manage their tokens
    • Implement token revocation
  3. Error Handling

    • Return appropriate status codes
    • Provide clear error messages
    • Log security-related events
  4. Performance

    • Use eager loading to avoid N+1 queries
    • Implement caching where appropriate
    • Paginate large result sets

9. Validation and Error Handling

9.1 Request Validation

Basic Validation in Controller

public function store(Request $request)
{
    $validated = $request->validate([
        'title' => 'required|string|max:255',
        'email' => 'required|email|unique:users',
        'age' => 'required|integer|min:18',
        'website' => 'nullable|url',
        'status' => 'in:active,inactive',
        'tags' => 'array|min:1|max:5',
        'tags.*' => 'string|max:50',
    ]);
}

Form Request Validation

// Generate form request
php artisan make:request StorePostRequest

// app/Http/Requests/StorePostRequest.php
namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class StorePostRequest extends FormRequest
{
    public function authorize()
    {
        return true;
    }

    public function rules()
    {
        return [
            'title' => 'required|string|max:255',
            'content' => 'required|string|min:10',
            'category_id' => 'required|exists:categories,id',
            'tags' => 'array|min:1|max:5',
            'tags.*' => 'integer|exists:tags,id',
            'publish_at' => 'nullable|date|after:today',
        ];
    }

    public function messages()
    {
        return [
            'title.required' => 'A title is required',
            'content.min' => 'Content must be at least 10 characters',
            'category_id.exists' => 'Selected category is invalid',
            'tags.*.exists' => 'One or more selected tags are invalid',
        ];
    }
}

9.2 Custom Validation Rules

// Generate custom rule
php artisan make:rule StrongPassword

// app/Rules/StrongPassword.php
namespace App\Rules;

use Illuminate\Contracts\Validation\Rule;

class StrongPassword implements Rule
{
    public function passes($attribute, $value)
    {
        return preg_match('/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/', $value);
    }

    public function message()
    {
        return 'The :attribute must contain at least one uppercase letter, one lowercase letter, one number, and one special character.';
    }
}

// Using custom rule
public function rules()
{
    return [
        'password' => ['required', new StrongPassword],
    ];
}

9.3 Error Handling

Custom Exception Handler

// app/Exceptions/Handler.php
namespace App\Exceptions;

use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Validation\ValidationException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Throwable;

class Handler extends ExceptionHandler
{
    public function register()
    {
        $this->renderable(function (ValidationException $e, $request) {
            return response()->json([
                'message' => 'The given data was invalid.',
                'errors' => $e->errors(),
            ], 422);
        });

        $this->renderable(function (ModelNotFoundException $e, $request) {
            return response()->json([
                'message' => 'Resource not found.',
            ], 404);
        });

        $this->renderable(function (NotFoundHttpException $e, $request) {
            return response()->json([
                'message' => 'The requested resource was not found.',
            ], 404);
        });
    }
}

Custom Exception

// app/Exceptions/InsufficientBalanceException.php
namespace App\Exceptions;

use Exception;

class InsufficientBalanceException extends Exception
{
    public function render($request)
    {
        return response()->json([
            'message' => 'Insufficient balance for this transaction.',
            'error_code' => 'INSUFFICIENT_BALANCE'
        ], 422);
    }
}

10. Testing and Debugging

10.1 Writing Tests

Unit Tests

namespace Tests\Unit;

use PHPUnit\Framework\TestCase;
use App\Services\PriceCalculator;

class PriceCalculatorTest extends TestCase
{
    public function test_calculates_total_with_tax()
    {
        $calculator = new PriceCalculator();
        
        $total = $calculator->calculateTotal(100, 0.1);
        
        $this->assertEquals(110, $total);
    }
}

Feature Tests

namespace Tests\Feature;

use Tests\TestCase;
use App\Models\User;
use App\Models\Post;
use Laravel\Sanctum\Sanctum;
use Illuminate\Foundation\Testing\RefreshDatabase;

class PostControllerTest extends TestCase
{
    use RefreshDatabase;

    public function setUp(): void
    {
        parent::setUp();
        $this->user = User::factory()->create();
    }

    public function test_can_get_posts_list()
    {
        Sanctum::actingAs($this->user);
        
        Post::factory()->count(3)->create([
            'user_id' => $this->user->id
        ]);

        $response = $this->getJson('/api/posts');

        $response->assertStatus(200)
                ->assertJsonCount(3, 'data')
                ->assertJsonStructure([
                    'data' => [
                        '*' => ['id', 'title', 'content', 'created_at']
                    ],
                    'meta' => ['current_page', 'last_page', 'per_page']
                ]);
    }

    public function test_can_create_post()
    {
        Sanctum::actingAs($this->user);

        $postData = [
            'title' => 'Test Post',
            'content' => 'Test Content'
        ];

        $response = $this->postJson('/api/posts', $postData);

        $response->assertStatus(201)
                ->assertJson([
                    'title' => $postData['title'],
                    'content' => $postData['content']
                ]);

        $this->assertDatabaseHas('posts', $postData);
    }
}

10.2 Debugging Tools

Using dd() and dump()

public function show($id)
{
    $user = User::find($id);
    dd($user); // Dumps data and dies
    dump($user); // Dumps data and continues execution
}

Laravel Telescope

# Install Telescope
composer require laravel/telescope --dev

# Publish assets and migrations
php artisan telescope:install
php artisan migrate
// config/telescope.php
return [
    'enabled' => env('TELESCOPE_ENABLED', true),
    'middleware' => [
        'web',
        \Laravel\Telescope\Http\Middleware\Authorize::class,
    ],
];

10.3 Testing Best Practices

  1. Test Organization

    • Group related tests in test classes
    • Use descriptive test method names
    • Follow the Arrange-Act-Assert pattern
    • Use test databases
  2. Test Data

    • Use factories for test data
    • Avoid hard-coded values
    • Clean up after tests
    • Use database transactions
  3. Testing Strategies

    • Test happy paths and edge cases
    • Test validation rules
    • Test authorization
    • Test API responses
  4. Performance Testing

public function test_endpoint_performance()
{
    $startTime = microtime(true);
    
    // Make request
    $response = $this->getJson('/api/posts');
    
    $endTime = microtime(true);
    $executionTime = ($endTime - $startTime) * 1000; // in milliseconds
    
    $this->assertLessThan(
        500, // 500ms threshold
        $executionTime,
        "API response too slow: {$executionTime}ms"
    );
}

10.4 Common Debugging Scenarios

  1. Database Issues
\DB::enableQueryLog();
// Your code here
dd(\DB::getQueryLog());
  1. Request/Response Debugging
public function show(Request $request)
{
    logger()->info('Request data:', [
        'headers' => $request->headers->all(),
        'body' => $request->all()
    ]);
    
    // Your code here
}
  1. Cache Debugging
// Clear cache during development
php artisan cache:clear
php artisan config:clear
php artisan route:clear
php artisan view:clear