03: Laravel's Developer Experience: Productivity, Conventions and Tools

Chapter 03: Laravel’s Developer Experience: Productivity, Conventions and Tools
Section titled “Chapter 03: Laravel’s Developer Experience: Productivity, Conventions and Tools”Overview
Section titled “Overview”In Chapter 02, you learned about modern PHP’s evolution—type declarations, JIT compilation, and language features that bring PHP on par with Python. Now it’s time to explore Laravel’s developer experience tools, which are where Laravel truly shines. If you’ve worked with Django’s manage.py or Flask’s CLI tools, you’ll find Laravel’s Artisan CLI familiar yet more powerful.
This chapter is about productivity and conventions. Laravel follows a “conventions over configuration” philosophy similar to Django, meaning you spend less time configuring and more time building. You’ll learn how Laravel’s command-line tools, migrations, testing framework, and naming conventions compare to Python equivalents—and where Laravel might surprise you with its developer-friendly features.
By the end of this chapter, you’ll understand that Laravel’s developer experience tools mirror Python’s best practices while adding their own delightful touches. You’ll master Artisan CLI (Laravel’s manage.py), migrations (Laravel’s version of Django migrations), PHPUnit testing (Laravel’s pytest), and Laravel’s conventions. Most importantly, you’ll see how these tools accelerate development in ways that feel familiar yet refreshing.
Prerequisites
Section titled “Prerequisites”Before starting this chapter, you should have:
- Completion of Chapter 02 or equivalent understanding of modern PHP
- Laravel 11.x installed (or ability to follow along with code examples)
- Python experience with Django’s
manage.pyor Flask CLI - Familiarity with pytest or unittest
- Basic understanding of database migrations (Django migrations preferred)
- Estimated Time: ~75 minutes
Verify your setup:
# Check PHP version (should show PHP 8.4+)php --version
# Check Composer is installedcomposer --version
# If you have Laravel installed, verify Artisan worksphp artisan --version
# Expected output: Laravel Framework 11.x.x (or similar)What You’ll Build
Section titled “What You’ll Build”By the end of this chapter, you will have:
- Understanding of Artisan CLI commands and how they compare to Django’s
manage.py - Knowledge of Laravel migrations workflow vs Django migrations
- PHPUnit test examples comparing to pytest
- Understanding of Laravel conventions vs Django/Flask conventions
- Working code examples demonstrating each tool
- Ability to create custom Artisan commands, migrations, seeders, and tests
- Confidence in Laravel’s developer experience tools
Quick Start
Section titled “Quick Start”Want to see Laravel’s developer tools in action right away? Here’s a quick comparison:
Django (Python):
# Create a migrationpython manage.py makemigrations
# Run migrationspython manage.py migrate
# Run testspython manage.py testLaravel (PHP):
# Create a migrationphp artisan make:migration create_users_table
# Run migrationsphp artisan migrate
# Run testsphp artisan testSee the pattern? Laravel’s php artisan is Django’s python manage.py equivalent. Both provide command-line access to framework functionality, but Laravel’s Artisan offers more built-in commands and a more consistent interface. This chapter will show you all the ways Laravel’s developer tools compare to Python equivalents.
Objectives
Section titled “Objectives”- Master Artisan CLI commands and compare to Python equivalents (Django’s
manage.py, Flask CLI) - Understand Laravel migrations vs Django migrations workflow
- Learn PHPUnit testing vs pytest workflows
- Understand Laravel conventions vs Django/Flask conventions
- Use seeders and factories for test data management
- Recognize Laravel’s “conventions over configuration” philosophy
- Create custom Artisan commands, migrations, seeders, and tests
- Use Laravel’s code generation commands (
make:*) to boost productivity - Manage cache and configuration with Artisan commands
Step 1: Artisan CLI - Laravel’s Command-Line Tool (~15 min)
Section titled “Step 1: Artisan CLI - Laravel’s Command-Line Tool (~15 min)”Understand Laravel’s Artisan CLI and how it compares to Django’s manage.py and Flask CLI tools.
Actions
Section titled “Actions”-
Artisan vs manage.py - The Basics
Laravel’s Artisan CLI is the equivalent of Django’s
manage.py. Both provide command-line access to framework functionality:Django (Python):
Terminal window # List all available commandspython manage.py help# Run a specific commandpython manage.py runserverpython manage.py createsuperuserpython manage.py shellLaravel (PHP):
Terminal window # List all available commandsphp artisan list# Run a specific commandphp artisan servephp artisan make:userphp artisan tinkerThe complete Artisan command examples are available in
artisan-commands.php. -
Common Commands Comparison
Here’s how common Django commands map to Laravel:
Django Command Laravel Command Purpose python manage.py runserverphp artisan serveStart development server python manage.py shellphp artisan tinkerInteractive shell python manage.py makemigrationsphp artisan make:migrationCreate migration python manage.py migratephp artisan migrateRun migrations python manage.py testphp artisan testRun tests python manage.py createsuperuserphp artisan make:userCreate admin user python manage.py collectstaticphp artisan storage:linkLink storage python manage.py showmigrationsphp artisan migrate:statusShow migration status python manage.py dbshellphp artisan dbOpen database shell python manage.py shell_plusphp artisan tinkerEnhanced shell with models -
Creating Custom Commands
Both frameworks allow you to create custom commands:
Django Management Command:
management/commands/send_emails.py from django.core.management.base import BaseCommandfrom django.core.mail import send_mailclass Command(BaseCommand):help = 'Send email notifications'def add_arguments(self, parser):parser.add_argument('--count', type=int, default=10)def handle(self, *args, **options):count = options['count']self.stdout.write(f'Sending {count} emails...')# Send emails logicself.stdout.write(self.style.SUCCESS('Emails sent!'))Laravel Artisan Command:
app/Console/Commands/SendEmails.php <?phpnamespace App\Console\Commands;use Illuminate\Console\Command;class SendEmails extends Command{protected $signature = 'emails:send {--count=10}';protected $description = 'Send email notifications';public function handle(): int{$count = (int) $this->option('count');$this->info("Sending {$count} emails...");// Send emails logic$this->info('Emails sent!');return Command::SUCCESS;}}Creating the command:
Terminal window # Django: Create file manually in management/commands/# Laravel: Use Artisan generatorphp artisan make:command SendEmailsThe custom command examples are available in
artisan-commands.phpanddjango-manage.py. -
Artisan Tinker - Interactive Shell
Laravel’s
tinkeris similar to Django’sshellbut more powerful:Django Shell:
Terminal window python manage.py shell>>> from myapp.models import User>>> User.objects.all()Laravel Tinker:
Terminal window php artisan tinker>>> User::all()>>> User::where('email', 'john@example.com')->first()>>> $user = User::factory()->create()Tinker provides full access to your Laravel application, including models, facades, and helpers. It’s perfect for quick database queries and testing.
-
Route Listing - Discover Your Routes
Laravel’s
route:listcommand shows all registered routes, similar to Django’s URL pattern inspection:Django (Python):
Terminal window # Django doesn't have a built-in command, but you can:python manage.py show_urls # Requires django-extensions# Or inspect urls.py manuallyLaravel (PHP):
Terminal window # List all routesphp artisan route:list# Filter by methodphp artisan route:list --method=GET# Filter by namephp artisan route:list --name=user# Show route detailsphp artisan route:list --columns=method,uri,name,actionThis is invaluable for debugging routing issues and discovering available endpoints.
-
Cache and Config Management
Laravel provides commands to manage cache and configuration:
Laravel (PHP):
Terminal window # Clear various cachesphp artisan cache:clear # Clear application cachephp artisan config:clear # Clear configuration cachephp artisan route:clear # Clear route cachephp artisan view:clear # Clear compiled view cache# Cache for production (optimization)php artisan config:cache # Cache configuration filesphp artisan route:cache # Cache routesphp artisan view:cache # Cache compiled viewsDjango (Python):
Terminal window # Django's equivalentpython manage.py collectstatic # Collect static filespython manage.py clearsessions # Clear session data# Django doesn't cache config/routes by default::: tip Production Optimization In production, use
php artisan config:cacheandphp artisan route:cacheto improve performance. These commands compile configuration and routes into optimized files. Remember to runconfig:clearandroute:clearafter making changes in development! ::: -
Development Server Options
Laravel’s development server offers more options than Django’s:
Django (Python):
Terminal window python manage.py runserverpython manage.py runserver 8000python manage.py runserver 0.0.0.0:8000Laravel (PHP):
Terminal window php artisan servephp artisan serve --port=8000php artisan serve --host=0.0.0.0php artisan serve --port=8080 --host=127.0.0.1Both serve your application locally, but Laravel’s server is built on PHP’s built-in server, while Django uses a development server written in Python.
-
Artisan’s Built-in Helpers
Laravel’s Artisan provides helpful output formatting:
# In an Artisan command$this->info('Success message'); // Green text$this->error('Error message'); // Red text$this->warn('Warning message'); // Yellow text$this->line('Plain message'); // Default text$this->table(['Name', 'Email'], $data); // Formatted tableDjango’s
manage.pyuses similar helpers:# In a Django management commandself.stdout.write(self.style.SUCCESS('Success message'))self.stdout.write(self.style.ERROR('Error message'))self.stdout.write(self.style.WARNING('Warning message'))
Expected Result
Section titled “Expected Result”After completing this step, you’ll understand that:
- Laravel’s
php artisanis equivalent to Django’spython manage.py - Artisan provides more built-in commands than Django’s
manage.py - Both frameworks allow custom command creation
- Artisan’s command syntax is more consistent and discoverable
- Laravel’s
tinkeris similar to Django’sshellbut more powerful - Artisan’s
make:*commands generate boilerplate code automatically - Artisan provides helpful output formatting methods
Why It Works
Section titled “Why It Works”Laravel’s Artisan CLI follows the same philosophy as Django’s manage.py: provide a single entry point for all framework operations. Artisan is built on Symfony Console component, which provides a robust command-line interface. The make:* commands generate boilerplate code, reducing manual file creation. Artisan’s help system (php artisan help <command>) provides inline documentation, making it easy to discover command options.
Troubleshooting
Section titled “Troubleshooting”- “Command not found” — Ensure you’re in a Laravel project directory. Artisan commands only work within Laravel projects. Check with
php artisan list. - “Artisan command doesn’t exist” — Some commands require specific packages. For example,
php artisan make:usermight require Laravel Breeze or Jetstream. Usephp artisan listto see available commands. - “Permission denied” — Ensure the
artisanfile is executable:chmod +x artisan(though this is rarely needed). - “Class not found” — After creating a custom command, run
composer dump-autoloadto refresh the autoloader. Laravel should auto-discover commands inapp/Console/Commands/.
Step 2: Database Migrations - Version Control for Your Schema (~20 min)
Section titled “Step 2: Database Migrations - Version Control for Your Schema (~20 min)”Master Laravel migrations and understand how they compare to Django migrations.
Actions
Section titled “Actions”-
Creating Migrations
Both frameworks use migrations for database schema versioning:
Django (Python):
Terminal window # Create a migrationpython manage.py makemigrations# Create a migration for a specific apppython manage.py makemigrations myapp# Create an empty migrationpython manage.py makemigrations --empty myappLaravel (PHP):
Terminal window # Create a migrationphp artisan make:migration create_users_table# Create a migration with specific namephp artisan make:migration add_email_to_users_table --table=users# Create an empty migrationphp artisan make:migration custom_migration -
Migration File Structure
Here’s how migration files compare:
Django Migration:
myapp/migrations/0001_initial.py from django.db import migrations, modelsclass Migration(migrations.Migration):dependencies = []operations = [migrations.CreateModel(name='User',fields=[('id', models.BigAutoField(primary_key=True)),('name', models.CharField(max_length=255)),('email', models.EmailField(max_length=255, unique=True)),('created_at', models.DateTimeField(auto_now_add=True)),],),]Laravel Migration:
database/migrations/2024_01_01_000001_create_users_table.php <?phpuse Illuminate\Database\Migrations\Migration;use Illuminate\Database\Schema\Blueprint;use Illuminate\Support\Facades\Schema;return new class extends Migration{public function up(): void{Schema::create('users', function (Blueprint $table) {$table->id();$table->string('name');$table->string('email')->unique();$table->timestamp('created_at')->useCurrent();});}public function down(): void{Schema::dropIfExists('users');}};::: tip Laravel Migration Naming Laravel migrations use timestamps in filenames (e.g.,
2024_01_01_120000_create_users_table.php). This ensures migrations run in chronological order, even if created on different machines. Django uses numbered migrations (0001_initial.py), which can cause conflicts in team environments. :::The migration examples are available in
laravel-migration.phpanddjango-migration.py. -
Running Migrations
Both frameworks track which migrations have been run:
Django (Python):
Terminal window # Run all pending migrationspython manage.py migrate# Run migrations for specific apppython manage.py migrate myapp# Show migration statuspython manage.py showmigrationsLaravel (PHP):
Terminal window # Run all pending migrationsphp artisan migrate# Run migrations in specific pathphp artisan migrate --path=database/migrations/custom# Show migration statusphp artisan migrate:status -
Rolling Back Migrations
Both frameworks support rolling back migrations:
Django (Python):
Terminal window # Rollback last migrationpython manage.py migrate myapp zeropython manage.py migrate myapp 0001# Rollback to specific migrationpython manage.py migrate myapp 0002_previous_migrationLaravel (PHP):
Terminal window # Rollback last batch of migrationsphp artisan migrate:rollback# Rollback specific number of stepsphp artisan migrate:rollback --step=3# Rollback all migrationsphp artisan migrate:reset# Rollback and re-run migrationsphp artisan migrate:refresh -
Migration Best Practices
Both frameworks follow similar best practices:
- Never edit existing migrations — Create new migrations for changes
- Keep migrations small — One logical change per migration
- Test migrations — Always test rollbacks
- Use descriptive names —
create_users_tablenotmigration1
Expected Result
Section titled “Expected Result”After completing this step, you’ll understand that:
- Laravel migrations work similarly to Django migrations
- Both use timestamp-based versioning (Django uses numbers, Laravel uses timestamps)
- Both track which migrations have been applied
- Both support rolling back migrations
- Laravel’s migration syntax is more fluent/expressive
- Laravel migrations are more explicit about up/down operations
Why It Works
Section titled “Why It Works”Migrations provide version control for database schemas, allowing teams to share schema changes and deploy consistently. Laravel’s migration system is built on top of a database abstraction layer, making it database-agnostic. The up() method defines what happens when migrating forward, while down() defines the rollback behavior. Laravel tracks migrations in a migrations table, similar to Django’s django_migrations table.
Troubleshooting
Section titled “Troubleshooting”- “Migration already exists” — Laravel uses timestamps in migration filenames. If you create a migration at the same second, you might get a conflict. Wait a second or manually adjust the timestamp.
- “Migration not found” — Ensure migrations are in
database/migrations/directory. Check file naming:YYYY_MM_DD_HHMMSS_description.php. - “Can’t rollback” — Ensure the
down()method is properly implemented. Laravel requires bothup()anddown()methods for rollbacks to work. - “Foreign key constraint fails” — Roll back migrations in reverse order. Use
php artisan migrate:rollback --step=Nto rollback multiple migrations. - “Migration already exists” — Laravel uses timestamps in filenames. If you create migrations at the same second, you might get conflicts. Wait a second or manually adjust the timestamp.
- “Table already exists” — Use
php artisan migrate:freshto drop all tables and re-run migrations (development only!). Or checkmigrationstable to see which migrations have run.
Step 3: Testing with PHPUnit - Laravel’s Testing Framework (~20 min)
Section titled “Step 3: Testing with PHPUnit - Laravel’s Testing Framework (~20 min)”Learn PHPUnit testing and understand how it compares to pytest and unittest.
Actions
Section titled “Actions”-
Test Structure Comparison
Here’s how test files compare:
pytest (Python):
tests/test_user.py import pytestfrom django.contrib.auth import get_user_modelUser = get_user_model()def test_user_creation():user = User.objects.create(name='John Doe',email='john@example.com')assert user.name == 'John Doe'assert user.email == 'john@example.com'def test_user_str():user = User(name='John Doe', email='john@example.com')assert str(user) == 'John Doe'PHPUnit (Laravel):
tests/Feature/UserTest.php <?phpnamespace Tests\Feature;use Tests\TestCase;use App\Models\User;class UserTest extends TestCase{public function test_user_creation(): void{$user = User::create(['name' => 'John Doe','email' => 'john@example.com',]);$this->assertEquals('John Doe', $user->name);$this->assertEquals('john@example.com', $user->email);}public function test_user_to_string(): void{$user = new User(['name' => 'John Doe','email' => 'john@example.com',]);$this->assertEquals('John Doe', (string) $user);}}The test examples are available in
phpunit-test.phpandpytest-test.py. -
Feature Tests vs Unit Tests
Laravel distinguishes between Feature tests (HTTP/Integration) and Unit tests (isolated):
Laravel Feature Test:
tests/Feature/UserApiTest.php <?phpnamespace Tests\Feature;use Tests\TestCase;use Illuminate\Foundation\Testing\RefreshDatabase;use App\Models\User;class UserApiTest extends TestCase{use RefreshDatabase; // Resets database after each testpublic function test_user_can_be_created_via_api(): void{$response = $this->postJson('/api/users', ['name' => 'John Doe','email' => 'john@example.com','password' => 'password123',]);$response->assertStatus(201)->assertJson(['name' => 'John Doe','email' => 'john@example.com',]);// Assert database has the user$this->assertDatabaseHas('users', ['email' => 'john@example.com',]);}}::: tip Laravel Testing Helpers Laravel provides many testing helpers beyond PHPUnit’s standard assertions. Use
$response->assertStatus(),$response->assertJson(),$this->assertDatabaseHas(), andRefreshDatabasetrait for comprehensive testing. These helpers make tests more readable and maintainable than raw PHPUnit assertions. :::Laravel Unit Test:
tests/Unit/UserServiceTest.php <?phpnamespace Tests\Unit;use Tests\TestCase;use App\Services\UserService;class UserServiceTest extends TestCase{public function test_user_service_creates_user(): void{$service = new UserService();$user = $service->createUser('John Doe', 'john@example.com');$this->assertEquals('John Doe', $user->name);}}pytest equivalent:
# Feature test (using Django test client)def test_user_can_be_created_via_api(client):response = client.post('/api/users/', {'name': 'John Doe','email': 'john@example.com',})assert response.status_code == 201assert response.json()['name'] == 'John Doe'# Unit test (isolated)def test_user_service_creates_user():service = UserService()user = service.create_user('John Doe', 'john@example.com')assert user.name == 'John Doe' -
Assertions Comparison
Both frameworks provide similar assertion methods:
pytest PHPUnit Purpose assert x == y$this->assertEquals($x, $y)Equality assertion assert x is None$this->assertNull($x)Null assertion assert x in list$this->assertContains($x, $list)Contains assertion assert response.status_code == 200$response->assertStatus(200)HTTP status assert 'text' in response.content$response->assertSee('text')Content assertion -
Running Tests
Both frameworks provide command-line test runners:
pytest (Python):
Terminal window # Run all testspytest# Run specific test filepytest tests/test_user.py# Run specific testpytest tests/test_user.py::test_user_creation# Run with coveragepytest --cov=app tests/PHPUnit (Laravel):
Terminal window # Run all testsphp artisan test# Run specific test filephp artisan test tests/Feature/UserTest.php# Run specific testphp artisan test --filter test_user_creation# Run with coveragephp artisan test --coverage -
Database Testing
Laravel provides database testing helpers:
tests/Feature/UserTest.php <?phpnamespace Tests\Feature;use Tests\TestCase;use Illuminate\Foundation\Testing\RefreshDatabase;use App\Models\User;class UserTest extends TestCase{use RefreshDatabase; // Resets database after each testpublic function test_user_can_be_created(): void{$user = User::factory()->create(['name' => 'John Doe',]);$this->assertDatabaseHas('users', ['name' => 'John Doe',]);}}Django equivalent:
tests/test_user.py from django.test import TestCasefrom django.contrib.auth import get_user_modelUser = get_user_model()class UserTest(TestCase):def test_user_can_be_created(self):user = User.objects.create(name='John Doe')self.assertTrue(User.objects.filter(name='John Doe').exists())
Expected Result
Section titled “Expected Result”After completing this step, you’ll understand that:
- PHPUnit is Laravel’s testing framework, equivalent to pytest/unittest
- Laravel distinguishes Feature tests (HTTP) from Unit tests (isolated)
- PHPUnit assertions are method-based (
$this->assertEquals()), while pytest uses functions (assert x == y) - Laravel provides database testing helpers (
RefreshDatabase,assertDatabaseHas) - Both frameworks support test discovery and filtering
- Laravel’s
php artisan testis more integrated than pytest’s standalone runner
Why It Works
Section titled “Why It Works”PHPUnit is PHP’s standard testing framework, similar to pytest for Python. Laravel extends PHPUnit with additional helpers for HTTP testing, database testing, and assertions. The RefreshDatabase trait automatically resets the database between tests, similar to Django’s TestCase. Laravel’s test runner (php artisan test) wraps PHPUnit with Laravel-specific features like automatic environment detection and better error reporting.
Troubleshooting
Section titled “Troubleshooting”- “Test database not found” — Laravel uses a separate test database. Ensure
.env.testingexists or setDB_DATABASEinphpunit.xml. Laravel will create the database automatically if it doesn’t exist. - “Migration errors in tests” — Use
RefreshDatabasetrait to ensure migrations run. Check thatdatabase/migrationsdirectory contains all migrations. - “Factory not found” — Ensure factories are in
database/factories/directory and properly namespaced. Usephp artisan make:factory ModelFactoryto create factories. - “Test takes too long” — Use
RefreshDatabaseinstead ofDatabaseMigrationsfor faster tests. Consider using in-memory SQLite for unit tests.
Step 4: Seeders and Factories - Test Data Management (~15 min)
Section titled “Step 4: Seeders and Factories - Test Data Management (~15 min)”Understand Laravel seeders and factories, and how they compare to Django fixtures and Faker.
Actions
Section titled “Actions”-
Creating Seeders
Seeders populate the database with initial or test data:
Django Fixture:
myapp/fixtures/users.json [{"model": "auth.user","pk": 1,"fields": {"name": "John Doe","email": "john@example.com"}}]Laravel Seeder:
database/seeders/UserSeeder.php <?phpnamespace Database\Seeders;use Illuminate\Database\Seeder;use App\Models\User;class UserSeeder extends Seeder{public function run(): void{User::create(['name' => 'John Doe','email' => 'john@example.com',]);}}Creating seeders:
Terminal window # Django: Create JSON file manually# Laravel: Use Artisan generatorphp artisan make:seeder UserSeederThe seeder examples are available in
laravel-seeder.phpanddjango-fixture.json. -
Running Seeders
Both frameworks provide ways to load seed data:
Django (Python):
Terminal window # Load fixturepython manage.py loaddata users.json# Load all fixturespython manage.py loaddata *Laravel (PHP):
Terminal window # Run specific seederphp artisan db:seed --class=UserSeeder# Run all seedersphp artisan db:seed# Migrate and seedphp artisan migrate --seed -
Using Factories for Test Data
Factories generate test data programmatically:
Django Factory (using factory_boy):
myapp/factories.py import factoryfrom django.contrib.auth import get_user_modelUser = get_user_model()class UserFactory(factory.django.DjangoModelFactory):class Meta:model = Username = factory.Faker('name')email = factory.Faker('email')Laravel Factory:
database/factories/UserFactory.php <?phpnamespace Database\Factories;use App\Models\User;use Illuminate\Database\Eloquent\Factories\Factory;use Illuminate\Support\Str;class UserFactory extends Factory{protected $model = User::class;public function definition(): array{return ['name' => fake()->name(),'email' => fake()->unique()->safeEmail(),'email_verified_at' => now(),'password' => bcrypt('password'), // Default password'remember_token' => Str::random(10),];}/*** Indicate that the user's email should be unverified.*/public function unverified(): static{return $this->state(fn (array $attributes) => ['email_verified_at' => null,]);}}::: tip Factory States Laravel factories support “states” that modify the default attributes. Use
User::factory()->unverified()->create()to create an unverified user. This is more flexible than Django’s factory_boy, which requires separate factory classes for variations. :::Using factories in tests:
Django:
# In a testuser = UserFactory.create(name='John Doe')users = UserFactory.create_batch(10)Laravel:
// In a test$user = User::factory()->create(['name' => 'John Doe']);$users = User::factory()->count(10)->create();The factory examples are available in
laravel-factory.php. -
DatabaseSeeder - Main Seeder
Laravel’s
DatabaseSeederis the main entry point for seeding:database/seeders/DatabaseSeeder.php <?phpnamespace Database\Seeders;use Illuminate\Database\Seeder;class DatabaseSeeder extends Seeder{public function run(): void{$this->call([UserSeeder::class,PostSeeder::class,]);}}Django equivalent (using management command):
management/commands/seed_data.py from django.core.management.base import BaseCommandfrom myapp.factories import UserFactory, PostFactoryclass Command(BaseCommand):def handle(self, *args, **options):UserFactory.create_batch(10)PostFactory.create_batch(20)
Expected Result
Section titled “Expected Result”After completing this step, you’ll understand that:
- Laravel seeders are programmatic (PHP code), while Django fixtures are data files (JSON/YAML)
- Laravel factories are built-in, while Django uses third-party
factory_boy - Both frameworks support generating test data programmatically
- Laravel’s factory syntax is more fluent (
User::factory()->create()) - Seeders run via
php artisan db:seed, similar to Django’sloaddatacommand - Laravel’s
DatabaseSeedercoordinates multiple seeders
Why It Works
Section titled “Why It Works”Seeders provide a way to populate databases with initial or test data. Laravel’s seeders are PHP classes, making them more flexible than JSON fixtures. Factories generate test data programmatically using Faker, similar to Python’s factory_boy. Laravel’s factories are integrated into the framework, while Django requires factory_boy as a third-party package. The DatabaseSeeder class acts as the main entry point, allowing you to coordinate multiple seeders.
Troubleshooting
Section titled “Troubleshooting”- “Seeder class not found” — Ensure seeders are in
database/seeders/directory and properly namespaced. Runcomposer dump-autoloadto refresh autoloader. - “Factory not found” — Ensure factories are in
database/factories/directory. Usephp artisan make:factory UserFactoryto create factories. Runcomposer dump-autoloadafter creating factories. - “Duplicate entry error” — Use
fake()->unique()for unique fields, or clear database before seeding:php artisan migrate:fresh --seed. - “Faker not working” — Laravel includes Faker by default. If you see errors, ensure you’re using
fake()helper or$this->fakerin factories. In Laravel 11+, usefake()instead of$this->faker.
Step 5: Laravel Conventions - “Conventions Over Configuration” (~10 min)
Section titled “Step 5: Laravel Conventions - “Conventions Over Configuration” (~10 min)”Understand Laravel’s naming conventions and how they compare to Django/Flask conventions.
Actions
Section titled “Actions”-
File Naming Conventions
Laravel follows strict naming conventions:
Laravel Django Flask Example User.php(Model)models.py(containsUser)models.py(containsUser)Model class UserController.phpviews.py(containsUserView)views.py(function-based)Controller/View 2024_01_01_create_users.php0001_initial.pyN/A (no migrations) Migration UserSeeder.phpfixtures/users.jsonN/A Seeder/Fixture UserFactory.phpfactories.py(factory_boy)N/A Factory -
Directory Structure Comparison
Laravel Structure:
app/├── Models/│ └── User.php├── Http/│ ├── Controllers/│ │ └── UserController.php│ └── Middleware/├── Console/│ └── Commands/database/├── migrations/├── seeders/└── factories/tests/├── Feature/└── Unit/Django Structure:
myapp/├── models.py├── views.py├── urls.py├── admin.pymigrations/├── 0001_initial.pytests/├── test_models.pyfixtures/└── users.json -
Naming Patterns
Laravel uses specific naming patterns:
Models:
// Laravel: Singular, PascalCaseclass User extends Model {}// Table: 'users' (plural, snake_case)// Django: Singular, PascalCaseclass User(models.Model):class Meta:db_table = 'users' # Optional, defaults to 'myapp_user'Controllers:
// Laravel: PascalCase with 'Controller' suffixclass UserController extends Controller {}// Django: Function-based or class-baseddef user_list(request): pass# orclass UserListView(ListView): passMigrations:
Timestamp_description.php // 2024_01_01_120000_create_users_table.php// Django: Numbered_description.py// 0001_initial.py -
Convention Benefits
Laravel’s conventions reduce configuration:
Laravel (Convention-based):
// Model automatically knows table name is 'users'class User extends Model {}// Route automatically maps to UserController@indexRoute::get('/users', [UserController::class, 'index']);Django (More explicit):
# Model explicitly defines table name (optional)class User(models.Model):class Meta:db_table = 'users'# URL explicitly maps to viewpath('users/', views.user_list, name='user_list')
Expected Result
Section titled “Expected Result”After completing this step, you’ll understand that:
- Laravel follows strict naming conventions (singular models, plural tables)
- Laravel’s directory structure is more organized (separate folders for Models, Controllers)
- Django uses file-based organization (
models.py,views.py) - Laravel’s conventions reduce boilerplate configuration
- Both frameworks follow “conventions over configuration” philosophy
- Laravel’s conventions are more opinionated than Django’s
Why It Works
Section titled “Why It Works”Laravel’s conventions reduce cognitive load by providing predictable patterns. If you know a model is User, you know the table is users, the controller is UserController, and the migration follows a timestamp pattern. This reduces configuration and makes code more discoverable. Django also follows conventions but is less opinionated, allowing more flexibility at the cost of more configuration.
Troubleshooting
Section titled “Troubleshooting”- “Model not found” — Ensure models are in
app/Models/directory and properly namespaced (App\Models\User). Usecomposer dump-autoloadto refresh autoloader. - “Table not found” — Laravel automatically pluralizes model names. If your table name differs, override
$tableproperty:protected $table = 'custom_table_name'; - “Controller not found” — Ensure controllers are in
app/Http/Controllers/directory. Usephp artisan make:controller UserControllerto create controllers. Runcomposer dump-autoloadafter creating controllers. - “Route not found” — Check route naming. Laravel uses
Route::get('/users', [UserController::class, 'index'])syntax. Ensure controller method exists. Usephp artisan route:listto see all registered routes. - “Table name doesn’t match” — Laravel automatically pluralizes model names. If your table name differs, override
$tableproperty:protected $table = 'custom_table_name';
Step 6: Code Generation - Laravel’s make:* Commands (~10 min)
Section titled “Step 6: Code Generation - Laravel’s make:* Commands (~10 min)”Understand Laravel’s code generation commands and how they compare to Django’s manual file creation approach.
Actions
Section titled “Actions”-
Code Generation Overview
Laravel’s
make:*commands generate boilerplate code automatically, saving significant time. Django doesn’t have equivalent generators—you create files manually:Django (Python) - Manual Creation:
Terminal window # Django: Create files manually# 1. Create models.py file# 2. Create views.py file# 3. Create urls.py file# 4. Create forms.py file (if needed)# 5. Write all boilerplate code yourselfLaravel (PHP) - Code Generation:
Terminal window # Laravel: Generate files with Artisanphp artisan make:model Postphp artisan make:controller PostControllerphp artisan make:request StorePostRequestphp artisan make:middleware CheckAge -
Model Generation
Laravel can generate models with migrations and factories:
Laravel:
Terminal window # Generate model onlyphp artisan make:model Post# Generate model with migrationphp artisan make:model Post -m# Generate model with migration and factoryphp artisan make:model Post -m -f# Generate model with migration, factory, seeder, and controllerphp artisan make:model Post -aDjango equivalent:
Terminal window # Django: Create model manually in models.py# Then create migrationpython manage.py makemigrationsThe generated Laravel model includes proper namespacing, type hints, and follows conventions:
app/Models/Post.php <?phpnamespace App\Models;use Illuminate\Database\Eloquent\Model;class Post extends Model{// Model code here} -
Controller Generation
Laravel can generate controllers with CRUD methods:
Laravel:
Terminal window # Generate basic controllerphp artisan make:controller PostController# Generate resource controller (CRUD methods)php artisan make:controller PostController --resource# Generate API resource controller (no create/edit views)php artisan make:controller PostController --apiDjango equivalent:
# Django: Create views.py manually# Or use class-based viewsfrom django.views.generic import ListView, DetailViewclass PostListView(ListView):model = PostThe
--resourceflag generates all CRUD methods:index(),create(),store(),show(),edit(),update(),destroy(). -
Form Request Validation
Laravel generates form request classes for validation:
Laravel:
Terminal window php artisan make:request StorePostRequestGenerated file:
app/Http/Requests/StorePostRequest.php <?phpnamespace App\Http\Requests;use Illuminate\Foundation\Http\FormRequest;class StorePostRequest extends FormRequest{public function authorize(): bool{return true;}public function rules(): array{return ['title' => 'required|string|max:255','content' => 'required|string',];}}Django equivalent:
# Django: Create forms.py manuallyfrom django import formsclass PostForm(forms.ModelForm):class Meta:model = Postfields = ['title', 'content'] -
Middleware Generation
Laravel generates middleware classes:
Laravel:
Terminal window php artisan make:middleware CheckAgeGenerated file:
app/Http/Middleware/CheckAge.php <?phpnamespace App\Http\Middleware;use Closure;use Illuminate\Http\Request;class CheckAge{public function handle(Request $request, Closure $next): mixed{// Middleware logic herereturn $next($request);}}Django equivalent:
# Django: Create middleware manuallyclass CheckAgeMiddleware:def __init__(self, get_response):self.get_response = get_response -
API Resource Generation
Laravel can generate API resources for transforming model data:
Laravel:
Terminal window php artisan make:resource PostResourceThis generates a resource class for formatting API responses, something Django REST Framework handles differently.
Expected Result
Section titled “Expected Result”After completing this step, you’ll understand that:
- Laravel’s
make:*commands generate boilerplate code automatically - Django requires manual file creation—no equivalent generators
- Laravel’s code generation follows conventions, reducing errors
- Flags like
-m,-f,--resourcecreate related files together - Code generation is a major productivity advantage in Laravel
Why It Works
Section titled “Why It Works”Laravel’s code generation commands follow the framework’s conventions, ensuring consistency across projects. The make:* commands create properly namespaced, type-hinted code that follows PSR standards. This reduces boilerplate writing and prevents common mistakes. Django’s approach requires more manual work but gives you complete control over file structure.
Troubleshooting
Section titled “Troubleshooting”- “Command not found” — Ensure you’re using Laravel 11.x. Some
make:*commands may vary by version. Usephp artisan listto see available commands. - “Generated file has wrong namespace” — Laravel auto-detects namespaces based on directory structure. Ensure you’re running commands from the project root.
- “Can’t find generated file” — Generated files go to standard Laravel directories:
app/Models/,app/Http/Controllers/, etc. Check these directories after generation.
Exercises
Section titled “Exercises”Test your understanding of Laravel’s developer experience tools by completing these exercises:
Exercise 1: Create a Custom Artisan Command (~15 min)
Section titled “Exercise 1: Create a Custom Artisan Command (~15 min)”Goal: Practice creating custom Artisan commands and compare to Django management commands.
Requirements:
- Create a custom Artisan command called
users:listthat displays all users in a table format - The command should accept an optional
--limitargument (default: 10) - Display users in a formatted table with columns: ID, Name, Email, Created At
- Compare your implementation to a Django management command that does the same
Validation: Test your implementation:
# Run the commandphp artisan users:list
# Run with limitphp artisan users:list --limit=5
# Expected: Formatted table with user dataReference: See artisan-commands.php for examples.
Exercise 2: Write a Migration and Seeder (~20 min)
Section titled “Exercise 2: Write a Migration and Seeder (~20 min)”Goal: Practice creating migrations and seeders, comparing to Django migrations and fixtures.
Requirements:
-
Create a migration for a
poststable with fields:id(primary key)title(string, required)content(text)user_id(foreign key to users table)created_atandupdated_at(timestamps)
-
Create a
PostSeederthat creates 10 posts with fake data -
Update
DatabaseSeederto callPostSeeder -
Run the migration and seeder
-
Compare your implementation to Django migrations and fixtures
Validation: Verify your implementation:
# Run migrationphp artisan migrate
# Run seederphp artisan db:seed --class=PostSeeder
# Check databasephp artisan tinker>>> Post::count() // Should return 10Reference: See laravel-migration.php and laravel-seeder.php for examples.
Exercise 3: Write a PHPUnit Test (~15 min)
Section titled “Exercise 3: Write a PHPUnit Test (~15 min)”Goal: Practice writing PHPUnit tests and compare to pytest tests.
Requirements:
-
Create a
PostTestfeature test that:- Tests creating a post via API
- Tests retrieving a post
- Tests updating a post
- Tests deleting a post
-
Use factories to create test data
-
Use
RefreshDatabasetrait for database isolation -
Compare your implementation to pytest tests
Validation: Run your tests:
# Run all testsphp artisan test
# Run specific testphp artisan test --filter PostTest
# Expected: All tests passReference: See phpunit-test.php for examples.
Exercise 4: Use Code Generation Commands (~10 min)
Section titled “Exercise 4: Use Code Generation Commands (~10 min)”Goal: Practice using Laravel’s make:* commands to generate boilerplate code.
Requirements:
-
Generate a
Postmodel with migration and factory:Terminal window php artisan make:model Post -m -f -
Generate a resource controller for posts:
Terminal window php artisan make:controller PostController --resource -
Generate a form request for storing posts:
Terminal window php artisan make:request StorePostRequest -
Inspect the generated files and compare to Django’s manual file creation approach
Validation: Verify the generated files:
# Check model was createdls app/Models/Post.php
# Check migration was createdls database/migrations/*_create_posts_table.php
# Check controller was createdls app/Http/Controllers/PostController.php
# Check request was createdls app/Http/Requests/StorePostRequest.phpReference: See code generation examples in artisan-commands.php.
Wrap-up
Section titled “Wrap-up”Congratulations! You’ve completed the Laravel developer experience chapter. You now understand:
- ✓ Artisan CLI commands and how they compare to Django’s
manage.py - ✓ Laravel migrations vs Django migrations workflow
- ✓ PHPUnit testing vs pytest workflows
- ✓ Laravel seeders and factories vs Django fixtures and factory_boy
- ✓ Laravel conventions vs Django/Flask conventions
- ✓ Laravel’s “conventions over configuration” philosophy
- ✓ How to create custom Artisan commands, migrations, seeders, and tests
- ✓ Laravel’s code generation commands (
make:*) and their productivity benefits - ✓ Cache and configuration management with Artisan commands
- ✓ Route listing and development server options
What You’ve Achieved
Section titled “What You’ve Achieved”You’ve built a comprehensive understanding of Laravel’s developer experience tools. You can see that Laravel’s tools mirror Python’s best practices while adding their own delightful touches. Artisan CLI provides more built-in commands than Django’s manage.py. Laravel’s code generation commands (make:*) are a major productivity advantage—Django has no equivalent generators, requiring manual file creation. Laravel migrations work similarly to Django migrations but with more expressive syntax. PHPUnit testing is equivalent to pytest, with Laravel-specific helpers for HTTP and database testing. Seeders and factories provide flexible test data management, and Laravel’s conventions reduce configuration overhead. Cache and configuration management commands help optimize applications for production.
Next Steps
Section titled “Next Steps”In Chapter 04, we’ll explore PHP syntax and language differences for Python developers. You’ll learn:
- Variable prefixes (
$) and how they compare to Python - OOP differences between PHP and Python
- Type declarations and how they compare to Python type hints
- Namespaces vs Python modules
- Side-by-side syntax comparisons
Your understanding of Laravel’s developer experience tools will help you appreciate PHP’s syntax choices and how they contribute to Laravel’s developer-friendly features. The conventions you’ve learned here (naming patterns, directory structure) will make PHP syntax feel more natural.
Code Examples
Section titled “Code Examples”All code examples from this chapter are available in the code/chapter-03/ directory:
- Artisan Commands:
artisan-commands.php— Custom Artisan command examples - Django manage.py:
django-manage.py— Django management command comparison - Laravel Migration:
laravel-migration.php— Laravel migration example - Django Migration:
django-migration.py— Django migration comparison - PHPUnit Test:
phpunit-test.php— PHPUnit test example - pytest Test:
pytest-test.py— pytest comparison example - Laravel Seeder:
laravel-seeder.php— Laravel seeder example - Django Fixture:
django-fixture.json— Django fixture comparison - Laravel Factory:
laravel-factory.php— Laravel factory example
See the README.md for detailed instructions on running each example.
Further Reading
Section titled “Further Reading”To deepen your understanding:
- Laravel Artisan Documentation — Complete Artisan CLI reference
- Laravel Migrations Guide — Database migrations documentation
- PHPUnit Documentation — PHPUnit testing framework
- Laravel Testing Guide — Laravel-specific testing features
- Laravel Seeders Documentation — Database seeding guide
- Laravel Factories Documentation — Model factories guide
- Django Management Commands — Django management commands reference
- pytest Documentation — pytest testing framework
::: tip Ready to Explore PHP Syntax? Head to Chapter 04: The PHP Syntax & Language Differences for Python Devs to learn about PHP syntax and how it compares to Python! :::