Skip to main content
MediaStream’s backend is built on Laravel 12 with PHP 8.2, providing a robust foundation for the media streaming management platform.

Project Structure

app/
├── Actions/
│   └── Fortify/              # User authentication actions
│       ├── CreateNewUser.php
│       ├── ResetUserPassword.php
│       └── PasswordValidationRules.php
├── Http/
│   ├── Controllers/
│   │   ├── Api/              # API endpoints for AJAX
│   │   ├── Web/              # Inertia page controllers
│   │   ├── Settings/         # User settings
│   │   └── Controller.php    # Base controller
│   ├── Middleware/
│   │   ├── HandleInertiaRequests.php
│   │   └── HandleAppearance.php
│   ├── Requests/             # Form request validation
│   └── Services/             # Business logic services
│       └── MediastreamService.php
├── Models/
│   └── User.php
└── Providers/
    ├── AppServiceProvider.php
    └── RouteServiceProvider.php

Controllers

MediaStream uses two types of controllers: Web Controllers (Inertia pages) and API Controllers (AJAX endpoints).

Web Controllers (Inertia)

Web controllers return Inertia responses that render Vue components:
<?php
// app/Http/Controllers/Web/SeriesController.php

namespace App\Http\Controllers\Web;

use App\Http\Controllers\Controller;
use App\Http\Services\MediastreamService;
use Illuminate\Http\Request;
use Inertia\Inertia;

class SeriesController extends Controller
{
    /**
     * Display a listing of TV series.
     */
    public function index()
    {
        $response = MediastreamService::request('/show', 'get');
        
        if ($response->successful()) {
            $data = $response->json('data');
        } else {
            $data = [];
        }
        
        return Inertia::render('(media)/series/index', [
            'data' => $data,
        ]);
    }
    
    /**
     * Show the form for creating a new series.
     */
    public function create()
    {
        return Inertia::render('(media)/series/create/index');
    }
    
    /**
     * Store a newly created series.
     */
    public function store(Request $request)
    {
        $validated = $request->validate([
            'series_name' => 'required|string|max:255',
            'series_type' => 'required|in:tvshow',
        ]);
        
        $response = MediastreamService::request('/show', 'post', [
            'title' => $validated['series_name'],
            'type' => $validated['series_type'],
        ]);
        
        if ($response->successful()) {
            return to_route('series.index')
                ->with('success', 'Serie creada exitosamente.');
        }
        
        return back()->withErrors([
            'api_error' => 'Error al crear la serie: ' . $response->body(),
        ]);
    }
    
    /**
     * Display the specified series with seasons and chapters.
     */
    public function show(Request $request)
    {
        $showId = $request->route('showId');
        
        // Fetch series details
        $response = MediastreamService::request("/show/{$showId}", 'get');
        $data = $response->successful() ? $response->json() : [];
        
        // Fetch seasons
        $responseSeasons = MediastreamService::request(
            "/show/{$showId}/season", 
            'get'
        );
        $seasons = $responseSeasons->successful() 
            ? $responseSeasons->json() 
            : [];
        
        // Fetch chapters for first season
        $chapters = [];
        if (!empty($data['seasons'][0]['_id'])) {
            $responseChapter = MediastreamService::request(
                "/show/{$showId}/season/{$data['seasons'][0]['_id']}/episode",
                'get'
            );
            $chapters = $responseChapter->successful() 
                ? $responseChapter->json('data') 
                : [];
        }
        
        return Inertia::render('(media)/series/[showId]/index', [
            'data' => $data,
            'showId' => $showId,
            'seasons' => $seasons['data'],
            'chapters' => $chapters,
        ]);
    }
}
Inertia controllers return data that becomes props in Vue components. No need to build a separate API!

API Controllers

API controllers return JSON responses for AJAX requests:
<?php
// app/Http/Controllers/Api/SeriesController.php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Http\Services\MediastreamService;
use Illuminate\Http\Request;

class SeriesController extends Controller
{
    /**
     * Display a listing of the resource.
     */
    public function index()
    {
        $response = MediastreamService::request('/show', 'get');
        
        return response()->json(
            $response->json(),
            $response->status()
        );
    }
    
    /**
     * Store a newly created resource.
     */
    public function store(Request $request)
    {
        $validated = $request->validate([
            'title' => 'required|string|max:255',
            'type' => 'required|in:tvshow,movie',
        ]);
        
        $response = MediastreamService::request('/show', 'post', $validated);
        
        return response()->json(
            $response->json(),
            $response->status()
        );
    }
}

Services

MediastreamService

Centralized service for external API integration:
<?php
// app/Http/Services/MediastreamService.php

namespace App\Http\Services;

use Illuminate\Support\Facades\Http;
use Illuminate\Http\Client\Response;

class MediastreamService
{
    /**
     * Make a request to the Mediastream API.
     *
     * @param string $endpoint API endpoint (e.g., '/show', '/show/123')
     * @param string $method HTTP method (get, post, put, delete)
     * @param array $data Request data
     * @return Response
     */
    public static function request(
        string $endpoint, 
        string $method = 'get', 
        array $data = []
    ): Response {
        $baseUrl = rtrim(env('MEDIASTREAM_API_URL'), '/');
        $url = $baseUrl . '/' . ltrim($endpoint, '/');
        
        $client = Http::withHeaders([
            'X-API-Token' => env('MEDIASTREAM_API_KEY'),
            'Accept' => 'application/json',
        ]);
        
        switch (strtolower($method)) {
            case 'post':
                return $client->post($url, $data);
            case 'put':
                return $client->put($url, $data);
            case 'delete':
                return $client->delete($url, $data);
            default:
                return $client->get($url, $data);
        }
    }
}
Environment Configuration:
# .env
MEDIASTREAM_API_URL=https://api.mediastream.example
MEDIASTREAM_API_KEY=your-api-key-here
Usage:
use App\Http\Services\MediastreamService;

// GET request
$response = MediastreamService::request('/show', 'get');

// POST request
$response = MediastreamService::request('/show', 'post', [
    'title' => 'Breaking Bad',
    'type' => 'tvshow',
]);

// Check response
if ($response->successful()) {
    $data = $response->json();
} else {
    $error = $response->body();
}
All external API calls should go through MediastreamService for consistent authentication and error handling.

Routing

Resource Routes

MediaStream uses Laravel resource routes with custom parameter names:
<?php
// routes/web.php

use App\Http\Controllers\Web\{SeriesController, SeasonController, ChapterController};
use Illuminate\Support\Facades\Route;

Route::middleware('auth')->group(function () {
    // Series routes
    Route::resource('series', SeriesController::class)
        ->parameters(['series' => 'showId']);
    
    // Nested season routes
    Route::resource('series.seasons', SeasonController::class)
        ->parameters([
            'series' => 'showId',
            'seasons' => 'seasonId',
        ]);
    
    // Nested chapter routes
    Route::resource('series.seasons.chapters', ChapterController::class)
        ->parameters([
            'series' => 'showId',
            'seasons' => 'seasonId',
            'chapters' => 'chapterId',
        ]);
});
This generates routes like:
  • GET /seriesSeriesController@index
  • GET /series/{showId}SeriesController@show
  • GET /series/{showId}/seasons/{seasonId}SeasonController@show

API Routes

<?php
// routes/api.php

use App\Http\Controllers\Api\{SeriesController, SeasonController};
use Illuminate\Support\Facades\Route;

Route::prefix('api')->as('api.')->group(function () {
    Route::apiResource('series', SeriesController::class)
        ->parameters(['series' => 'showId']);
    
    Route::apiResource('series.seasons', SeasonController::class)
        ->parameters([
            'series' => 'showId',
            'seasons' => 'seasonId',
        ]);
});
API routes are prefixed with /api and return JSON responses.

Validation

Inline Validation

public function store(Request $request)
{
    $validated = $request->validate([
        'series_name' => 'required|string|max:255',
        'series_type' => 'required|in:tvshow,movie',
        'description' => 'nullable|string|max:1000',
        'release_year' => 'nullable|integer|min:1900|max:' . date('Y'),
    ]);
    
    // Use $validated data...
}

Form Request Classes

For complex validation, use Form Request classes:
<?php
// app/Http/Requests/Settings/ProfileUpdateRequest.php

namespace App\Http\Requests\Settings;

use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;

class ProfileUpdateRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     */
    public function authorize(): bool
    {
        return true;
    }
    
    /**
     * Get the validation rules that apply to the request.
     */
    public function rules(): array
    {
        return [
            'name' => ['required', 'string', 'max:255'],
            'email' => [
                'required', 
                'string', 
                'email', 
                'max:255',
                Rule::unique('users')->ignore($this->user()->id),
            ],
        ];
    }
}
Usage:
public function update(ProfileUpdateRequest $request)
{
    $request->user()->update($request->validated());
    
    return back()->with('success', 'Profile updated successfully.');
}

Middleware

HandleInertiaRequests

Shares global data with all Inertia pages:
<?php
// app/Http/Middleware/HandleInertiaRequests.php

namespace App\Http\Middleware;

use Illuminate\Foundation\Inspiring;
use Illuminate\Http\Request;
use Inertia\Middleware;

class HandleInertiaRequests extends Middleware
{
    protected $rootView = 'app';
    
    public function share(Request $request): array
    {
        [$message, $author] = str(Inspiring::quotes()->random())
            ->explode('-');
        
        return [
            ...parent::share($request),
            'name' => config('app.name'),
            'quote' => [
                'message' => trim($message), 
                'author' => trim($author)
            ],
            'auth' => [
                'user' => $request->user(),
            ],
            'sidebarOpen' => ! $request->hasCookie('sidebar_state') 
                || $request->cookie('sidebar_state') === 'true',
        ];
    }
}
This data is available in all Vue components via $page.props.
Shared data is sent with every request. Keep it lightweight - only share what’s needed globally.

Models

User Model

<?php
// app/Models/User.php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Fortify\TwoFactorAuthenticatable;

class User extends Authenticatable
{
    use HasFactory, Notifiable, TwoFactorAuthenticatable;
    
    protected $fillable = [
        'name',
        'email',
        'password',
    ];
    
    protected $hidden = [
        'password',
        'two_factor_secret',
        'two_factor_recovery_codes',
        'remember_token',
    ];
    
    protected function casts(): array
    {
        return [
            'email_verified_at' => 'datetime',
            'password' => 'hashed',
            'two_factor_confirmed_at' => 'datetime',
        ];
    }
}

Authentication

Laravel Fortify

MediaStream uses Laravel Fortify for authentication:
<?php
// app/Actions/Fortify/CreateNewUser.php

namespace App\Actions\Fortify;

use App\Models\User;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
use Laravel\Fortify\Contracts\CreatesNewUsers;

class CreateNewUser implements CreatesNewUsers
{
    use PasswordValidationRules;
    
    public function create(array $input): User
    {
        Validator::make($input, [
            'name' => ['required', 'string', 'max:255'],
            'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
            'password' => $this->passwordRules(),
        ])->validate();
        
        return User::create([
            'name' => $input['name'],
            'email' => $input['email'],
            'password' => Hash::make($input['password']),
        ]);
    }
}
Features enabled:
  • User registration
  • Login/logout
  • Password reset
  • Two-factor authentication (TOTP)
  • Password confirmation

Protecting Routes

Route::middleware('auth')->group(function () {
    // Authenticated routes
});

Route::middleware(['auth', 'verified'])->group(function () {
    // Requires email verification
});

Testing

MediaStream uses Pest PHP for testing:
<?php
// tests/Feature/SeriesTest.php

use App\Models\User;
use function Pest\Laravel\{actingAs, get, post};

it('displays series index page', function () {
    $user = User::factory()->create();
    
    actingAs($user)
        ->get('/series')
        ->assertOk()
        ->assertInertia(fn ($page) => $page
            ->component('(media)/series/index')
            ->has('data')
        );
});

it('creates a new series', function () {
    $user = User::factory()->create();
    
    actingAs($user)
        ->post('/series', [
            'series_name' => 'Breaking Bad',
            'series_type' => 'tvshow',
        ])
        ->assertRedirect('/series');
});
Run tests:
php artisan test
# or
composer test

Best Practices

  • Keep controllers thin - move business logic to services
  • Return early for error conditions
  • Use form requests for complex validation
  • Type-hint dependencies for automatic injection
  • Create services for complex business logic
  • Make services testable by injecting dependencies
  • Use static methods for simple utility functions
  • Keep external API calls in dedicated service classes
  • Always validate user input
  • Use Form Request classes for reusable validation
  • Return descriptive error messages
  • Validate early, before processing data
  • Check API response status before using data
  • Return appropriate HTTP status codes
  • Log errors for debugging
  • Show user-friendly error messages

Development Tools

Laravel Pail

Real-time log viewing:
php artisan pail

Tinker

Interactive REPL:
php artisan tinker
>>> $user = User::first();
>>> $user->name;

Code Style

Format code with Laravel Pint:
./vendor/bin/pint

Next Steps

Database Schema

Learn about migrations and database structure

Frontend Development

Explore Vue.js components and TypeScript

Build docs developers (and LLMs) love