Skip to main content

🚀 Lesson 24: Deployment, Next Steps & Best Practices

You've built a complete foundation in PHP — from syntax basics to database-driven applications with security protections. Now it's time to take your work live. In this final lesson, you'll learn how to deploy a PHP application to a hosting provider, use Composer to manage dependencies, and chart a path forward into the PHP ecosystem with frameworks, APIs, and advanced tools.

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Deploy a PHP application to a free or paid hosting provider
  • Configure environment variables and production settings
  • Install and use Composer for dependency management
  • Understand the role of PHP frameworks and when to use them
  • Build a basic REST API endpoint with PHP
  • Plan your continued learning path in the PHP ecosystem

Estimated Time: 30 minutes

Prerequisites: All previous lessons

📑 In This Lesson

Preparing for Production

Before deploying, you need to switch from development mode to production mode. The differences are critical — development settings that help you debug can become security vulnerabilities in production.

The Production Checklist

Setting Development Production Why
display_errors On Off Error messages expose file paths, SQL queries, and stack traces to attackers
error_reporting E_ALL E_ALL Still report all errors — but log them, don't display them
log_errors On On Write errors to a log file for later review
error_log (default) Specific path Control where error logs are written
Session cookies Default Secure, HttpOnly, SameSite Prevents cookie theft and CSRF (Lesson 23)
Database credentials In config file In environment variables Keeps secrets out of your code repository

Creating a Production php.ini Override

On shared hosting, you may not have access to the main php.ini. You can create a .user.ini file in your project root to override settings:

; File: .user.ini (place in project root)
; Production settings

display_errors = Off
log_errors = On
error_reporting = E_ALL
error_log = /home/youruser/logs/php_errors.log

; Session security
session.cookie_secure = On
session.cookie_httponly = On
session.cookie_samesite = Lax
session.use_strict_mode = On

; Upload limits (adjust as needed)
upload_max_filesize = 10M
post_max_size = 12M

⚠️ Never Commit Credentials

Database passwords, API keys, and other secrets should never appear in your code files. If you use Git, add config files with credentials to .gitignore. Use environment variables instead (covered in Section 4).

Deployment Options

PHP runs on nearly every hosting platform. Here's an overview of your options, from free to professional:

Option Cost Best For PHP + MySQL
InfinityFree Free Learning, portfolio projects Yes — PHP 8.x, MySQL, free subdomain + custom domain
000webhost Free Small projects, testing Yes — PHP, MySQL, limited resources
Shared Hosting
(Hostinger, SiteGround, A2)
$3–10/mo Personal sites, small business, WordPress Yes — cPanel, one-click installs, email
VPS
(DigitalOcean, Linode, Vultr)
$5–20/mo Full control, multiple apps, learning sysadmin Yes — you install and configure everything
PaaS
(Railway, Render, Laravel Forge)
$5–25/mo Deploy from Git, managed infrastructure Yes — automatic deployments, managed databases
Cloud
(AWS, Google Cloud, Azure)
Pay-per-use Enterprise, high traffic, microservices Yes — fully managed services, auto-scaling

✅ Recommendation for Learners

Start with InfinityFree (free) to deploy your first project and see it live. When you're ready for something more robust, try shared hosting ($3–5/month) for real projects, or a VPS ($5/month on DigitalOcean or Vultr) if you want to learn server administration. For modern deployment workflows, Railway or Render let you deploy directly from a Git repository.

Deploying to Shared Hosting

Most shared hosting uses cPanel with a file manager and FTP access. Here's the typical workflow:

flowchart LR LOCAL["💻 Local Dev
LAMP on WSL"] -->|"FTP / File Manager"| HOST["🌐 Shared Host
cPanel"] HOST --> DB["🗄️ Create MySQL DB
via phpMyAdmin"] HOST --> CONFIG["⚙️ Update Config
DB credentials"] HOST --> TEST["✅ Test Live Site"]

Step-by-Step Deployment

1. Sign Up and Access cPanel

Create an account on your chosen host. You'll receive cPanel login credentials (usually at yourdomain.com/cpanel or yourdomain.com:2083).

2. Create the MySQL Database

  1. In cPanel, go to MySQL Databases
  2. Create a new database (e.g., youruser_taskmanager)
  3. Create a new database user with a strong password
  4. Add the user to the database with All Privileges
  5. Open phpMyAdmin and run your setup.sql to create tables

3. Upload Your Files

Use cPanel's File Manager or an FTP client like FileZilla:

  1. Navigate to public_html/ (or a subdirectory like public_html/taskmanager/)
  2. Upload all your PHP files and directories
  3. Set file permissions: 644 for files, 755 for directories
  4. Ensure the uploads/ directory is writable: 755

4. Update Database Configuration

<?php
// File: config/database.php (production version)

return [
    "host"   => "localhost",                    // Usually localhost on shared hosting
    "dbname" => "youruser_taskmanager",         // Prefixed with your cPanel username
    "user"   => "youruser_taskdb",              // The DB user you created
    "pass"   => "your_strong_password_here",    // The password you set
];

5. Test Everything

  • Visit your site in the browser
  • Test all CRUD operations
  • Check that uploads work (correct permissions)
  • Verify that errors are logged, not displayed
  • Test on mobile

📖 Free Hosting with InfinityFree

InfinityFree provides free PHP + MySQL hosting with a custom subdomain (e.g., yoursite.infinityfreeapp.com). The process is similar to paid shared hosting: create a database in their panel, upload files via their file manager or FTP, and update your config. Limitations include no SSH access, limited bandwidth, and occasional downtime — but it's perfect for learning deployment.

Environment Variables

Hardcoding database credentials in your config file works for learning, but in the real world you want to keep secrets out of your codebase entirely. Environment variables solve this — they're set on the server, not in your code.

Reading Environment Variables in PHP

<?php
// File: config/database.php (environment-variable version)

return [
    "host"   => getenv("DB_HOST") ?: "localhost",
    "dbname" => getenv("DB_NAME") ?: "task_manager",
    "user"   => getenv("DB_USER") ?: "root",
    "pass"   => getenv("DB_PASS") ?: "",
];

Setting Environment Variables

How you set them depends on your environment:

# Option 1: Apache (.htaccess or virtual host config)
SetEnv DB_HOST "localhost"
SetEnv DB_NAME "task_manager"
SetEnv DB_USER "app_user"
SetEnv DB_PASS "s3cur3_p@ssw0rd"

# Option 2: Linux shell (for CLI scripts or cron jobs)
export DB_HOST="localhost"
export DB_NAME="task_manager"
export DB_USER="app_user"
export DB_PASS="s3cur3_p@ssw0rd"

# Option 3: .env file (with a library like vlucas/phpdotenv — see Composer section)

Using .env Files with Composer

The most popular approach in modern PHP is a .env file loaded by the vlucas/phpdotenv library:

# File: .env (in project root — NEVER commit this to Git!)

DB_HOST=localhost
DB_NAME=task_manager
DB_USER=app_user
DB_PASS=s3cur3_p@ssw0rd

APP_ENV=production
APP_DEBUG=false
<?php
// Load .env variables (after installing phpdotenv via Composer)
require __DIR__ . "/vendor/autoload.php";

$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
$dotenv->load();

// Now access them with $_ENV or getenv()
$dbHost = $_ENV["DB_HOST"];
$dbName = $_ENV["DB_NAME"];

⚠️ Always Add .env to .gitignore

# File: .gitignore
.env
config/database.php   # If it contains credentials
/vendor/              # Composer dependencies (reinstalled via composer install)
/uploads/*            # User uploads
!/uploads/.gitkeep    # Keep the directory in Git

Commit a .env.example file with placeholder values so other developers know which variables are needed.

Introduction to Composer

Composer is PHP's dependency manager — it's how you install and manage third-party libraries. Think of it as npm for PHP. Nearly every modern PHP project uses Composer, and understanding it is essential for working with frameworks.

Installing Composer

# On Ubuntu/WSL (your development environment):
sudo apt update
sudo apt install php-cli unzip
curl -sS https://getcomposer.org/installer | php
sudo mv composer.phar /usr/local/bin/composer

# Verify installation:
composer --version
# Output: Composer version 2.x.x 2025-xx-xx

Initializing a Project

# Navigate to your project directory
cd ~/projects/task-manager

# Initialize Composer (creates composer.json)
composer init

# It will ask for:
# - Package name (e.g., yourname/task-manager)
# - Description
# - Author
# - Minimum stability
# - License
# - Dependencies (you can skip and add later)

Installing Packages

# Install a package (e.g., phpdotenv for .env file support)
composer require vlucas/phpdotenv

# Install PHPMailer for email
composer require phpmailer/phpmailer

# Install a development-only tool (e.g., a testing framework)
composer require --dev phpunit/phpunit

Composer creates several files:

File/Directory Purpose Commit to Git?
composer.json Lists your dependencies and their version constraints Yes
composer.lock Records the exact versions installed (ensures consistency) Yes
vendor/ Contains all downloaded packages + the autoloader No — add to .gitignore

Using the Autoloader

Composer generates an autoloader that handles require statements for all installed packages. Include it once at the top of your application:

<?php
// Include Composer's autoloader — this is the ONLY require you need
// for all Composer packages
require __DIR__ . "/vendor/autoload.php";

// Now you can use any installed package without manual requires
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\Exception;

$mail = new PHPMailer(true);
// ... configure and send email ...

Autoloading Your Own Classes

You can also tell Composer to autoload your own classes using PSR-4:

{
    "name": "yourname/task-manager",
    "require": {
        "vlucas/phpdotenv": "^5.5",
        "phpmailer/phpmailer": "^6.9"
    },
    "autoload": {
        "psr-4": {
            "App\\": "classes/"
        }
    }
}
# After editing composer.json, regenerate the autoloader:
composer dump-autoload
<?php
// File: classes/TaskRepository.php
namespace App;

class TaskRepository {
    // ... your existing code ...
}

// File: index.php
require __DIR__ . "/vendor/autoload.php";

use App\TaskRepository;

$repo = new TaskRepository();
// No more manual require_once statements!

✅ Essential Composer Commands

Command What It Does
composer init Create a new composer.json interactively
composer require package/name Install a package and add it to composer.json
composer install Install all packages from composer.lock
composer update Update packages to latest versions within constraints
composer dump-autoload Regenerate the autoloader (after changing autoload config)
composer remove package/name Remove a package
composer show List all installed packages

Building a REST API

So far, all your PHP pages return HTML. But modern web applications often need to return JSON data — for JavaScript frontends (React, Vue), mobile apps, or third-party integrations. A REST API does exactly this: it receives HTTP requests and returns structured JSON responses.

A Simple JSON API Endpoint

<?php
// File: api/tasks.php
// A minimal REST API for tasks

require_once __DIR__ . "/../classes/TaskRepository.php";

// Set the response type to JSON
header("Content-Type: application/json; charset=UTF-8");

// Allow cross-origin requests (for JavaScript frontends on other domains)
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS");
header("Access-Control-Allow-Headers: Content-Type");

// Handle preflight OPTIONS requests
if ($_SERVER["REQUEST_METHOD"] === "OPTIONS") {
    http_response_code(204);
    exit;
}

$repo = new TaskRepository();
$method = $_SERVER["REQUEST_METHOD"];

// Simple routing based on HTTP method and URL
// GET /api/tasks.php       → list all tasks
// GET /api/tasks.php?id=5  → get one task
// POST /api/tasks.php      → create a task
// DELETE /api/tasks.php?id=5 → delete a task

try {
    switch ($method) {
        case "GET":
            $id = filter_input(INPUT_GET, "id", FILTER_VALIDATE_INT);

            if ($id) {
                // Get a single task
                $task = $repo->findById($id);
                if ($task) {
                    echo json_encode(["data" => $task]);
                } else {
                    http_response_code(404);
                    echo json_encode(["error" => "Task not found"]);
                }
            } else {
                // List all tasks
                $status = $_GET["status"] ?? null;
                $search = $_GET["q"] ?? null;
                $tasks = $repo->findAll($status, $search);
                echo json_encode(["data" => $tasks, "count" => count($tasks)]);
            }
            break;

        case "POST":
            // Read JSON body
            $input = json_decode(file_get_contents("php://input"), true);

            if (!$input || empty($input["title"])) {
                http_response_code(400);
                echo json_encode(["error" => "Title is required"]);
                break;
            }

            $newId = $repo->create(
                $input["title"],
                $input["description"] ?? null,
                $input["priority"] ?? "medium"
            );

            http_response_code(201);
            echo json_encode([
                "message" => "Task created",
                "data"    => $repo->findById($newId),
            ]);
            break;

        case "DELETE":
            $id = filter_input(INPUT_GET, "id", FILTER_VALIDATE_INT);

            if (!$id) {
                http_response_code(400);
                echo json_encode(["error" => "Valid task ID required"]);
                break;
            }

            if ($repo->delete($id)) {
                echo json_encode(["message" => "Task deleted"]);
            } else {
                http_response_code(404);
                echo json_encode(["error" => "Task not found"]);
            }
            break;

        default:
            http_response_code(405);
            echo json_encode(["error" => "Method not allowed"]);
    }
} catch (Exception $e) {
    error_log("API error: " . $e->getMessage());
    http_response_code(500);
    echo json_encode(["error" => "Internal server error"]);
}

Testing the API

# List all tasks
curl http://localhost/task-manager/api/tasks.php

# Get a single task
curl http://localhost/task-manager/api/tasks.php?id=1

# Create a task (send JSON in the body)
curl -X POST http://localhost/task-manager/api/tasks.php \
     -H "Content-Type: application/json" \
     -d '{"title": "Learn REST APIs", "priority": "high"}'

# Delete a task
curl -X DELETE http://localhost/task-manager/api/tasks.php?id=5

📖 Key API Concepts

  • Content-Type: application/json — Tells the client the response is JSON, not HTML
  • http_response_code() — Sets the HTTP status code: 200 (OK), 201 (Created), 400 (Bad Request), 404 (Not Found), 500 (Server Error)
  • php://input — Reads the raw request body (for JSON POST data, since $_POST only works with form-encoded data)
  • json_encode() / json_decode() — Convert between PHP arrays and JSON strings
  • CORS headers — Required when a JavaScript frontend on a different domain calls your API

PHP Frameworks

Everything in this course has been "vanilla" PHP — no frameworks, no libraries (except PDO). This was intentional: you need to understand how PHP works before a framework can help you. Now that you do, let's look at what frameworks offer and when to use them.

What Frameworks Provide

A framework gives you a pre-built structure and tools for common tasks so you can focus on your application's unique logic:

  • Routing: Map URLs to controller functions (no more if/switch on $_SERVER["REQUEST_METHOD"])
  • ORM (Object-Relational Mapping): Interact with the database using PHP objects instead of raw SQL
  • Templates: A templating engine with automatic escaping (no more manual htmlspecialchars())
  • Authentication: Login, registration, password reset — built-in
  • CSRF protection: Automatic token generation and validation on all forms
  • Testing: Built-in support for unit and integration tests
  • CLI tools: Code generators, database migrations, task scheduling

The Major PHP Frameworks

Framework Philosophy Best For Learning Curve
Laravel Elegant, full-featured, "batteries included" Web apps, APIs, SaaS products — the most popular choice Moderate
Symfony Modular, enterprise-grade, standards-based Large-scale enterprise applications, reusable components Steeper
Slim Minimal micro-framework REST APIs, small projects, learning MVC patterns Low
CodeIgniter Lightweight, low configuration Simple web apps, shared hosting (small footprint) Low

Laravel: A Taste

To give you a sense of what framework code looks like, here's the Task Manager's list and create functionality in Laravel:

<?php
// File: routes/web.php (Laravel routing)
// Each line maps a URL + HTTP method to a controller method

use App\Http\Controllers\TaskController;

Route::get("/tasks", [TaskController::class, "index"]);       // List
Route::get("/tasks/create", [TaskController::class, "create"]);// Show form
Route::post("/tasks", [TaskController::class, "store"]);       // Save
Route::get("/tasks/{task}/edit", [TaskController::class, "edit"]);
Route::put("/tasks/{task}", [TaskController::class, "update"]);
Route::delete("/tasks/{task}", [TaskController::class, "destroy"]);
<?php
// File: app/Http/Controllers/TaskController.php

namespace App\Http\Controllers;

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

class TaskController extends Controller
{
    public function index(Request $request)
    {
        // Eloquent ORM — no raw SQL needed
        $tasks = Task::query()
            ->when($request->status, fn($q, $s) => $q->where("status", $s))
            ->when($request->q, fn($q, $s) => $q->where("title", "like", "%$s%"))
            ->orderByRaw("FIELD(priority, 'high', 'medium', 'low')")
            ->get();

        // Return a Blade template with the data
        return view("tasks.index", compact("tasks"));
    }

    public function store(Request $request)
    {
        // Validation — one line, with automatic error handling
        $validated = $request->validate([
            "title"    => "required|max:255",
            "priority" => "required|in:low,medium,high",
        ]);

        // Create the task (mass assignment with validated data)
        Task::create($validated);

        // Flash message + redirect (PRG) — built-in
        return redirect("/tasks")->with("success", "Task created!");
    }
}

Notice how many things you built manually in this course — routing, validation, flash messages, CSRF protection, database queries — are handled automatically by the framework. That's the power of a framework: it eliminates boilerplate. But you understand what it's doing under the hood because you built it yourself first.

✅ When to Use a Framework

  • Use a framework when building a real application that needs authentication, database interactions, form handling, and maintainability over time
  • Use vanilla PHP for small scripts, simple APIs, learning exercises, or when you need maximum control and minimal overhead
  • Start with Laravel if you're unsure — it has the largest community, best documentation, and most job listings

Your Learning Roadmap

You've completed PHP Foundations. Here's where to go next, organized by interest area:

flowchart TD PHP["✅ PHP Foundations
(This Course)"] PHP --> FW["🏗️ Frameworks
Laravel / Symfony"] PHP --> API["🔌 REST APIs
Slim / Laravel API"] PHP --> WP["📝 WordPress
Theme & Plugin Dev"] PHP --> ADV["🧠 Advanced PHP
Design Patterns, Testing"] FW --> FULL["🚀 Full-Stack Dev
Laravel + Vue/React"] API --> MICRO["⚡ Microservices
Docker, Queues"] WP --> WPEXT["🔧 WordPress Ecosystem
WooCommerce, Gutenberg"] ADV --> ARCH["🏛️ Architecture
DDD, CQRS, Event Sourcing"]

Path 1: Full-Stack Web Development

  1. Learn Laravel — Follow the official Laravel Bootcamp (free, hands-on)
  2. Blade Templates — Laravel's templating engine with automatic XSS protection
  3. Eloquent ORM — Database interactions with PHP objects instead of SQL
  4. Laravel + Inertia.js + Vue/React — Modern SPA-like experience with server-side routing
  5. Livewire — Build reactive UIs without writing JavaScript

Path 2: API Development

  1. RESTful API design — Resource naming, HTTP methods, status codes, pagination
  2. Authentication — JWT tokens, OAuth 2.0, Laravel Sanctum/Passport
  3. API documentation — OpenAPI/Swagger specifications
  4. Rate limiting & caching — Protect and optimize your API
  5. GraphQL — Alternative to REST for flexible data fetching

Path 3: WordPress Development

  1. Theme development — The template hierarchy, the Loop, custom post types
  2. Plugin development — Hooks (actions/filters), shortcodes, settings pages
  3. WordPress REST API — Headless WordPress with a JavaScript frontend
  4. WooCommerce — E-commerce development
  5. Block development (Gutenberg) — Custom blocks with React

The next course in this learning path — PHP & WordPress — covers this in depth.

Path 4: Advanced PHP

  1. Design patterns — Repository, Factory, Strategy, Observer, Dependency Injection
  2. Testing — PHPUnit for unit tests, Pest for expressive testing, TDD workflow
  3. PHP 8.x features — Enums, fibers, readonly properties, named arguments, attributes
  4. Performance — OPcache, profiling with Xdebug, query optimization
  5. DevOps — Docker, CI/CD pipelines, GitHub Actions, deployment automation

Essential Tools to Learn

Tool Purpose Priority
Git Version control — track changes, collaborate, deploy Essential
Composer Dependency management (covered in this lesson) Essential
PHPUnit / Pest Automated testing High
Xdebug Step-through debugging and profiling High
Docker Containerized development environments Medium
Redis / Memcached Caching and session storage Medium
PHPStan / Psalm Static analysis — catch bugs without running code Medium

Summary & Course Wrap-Up

🎉 Lesson 24 Key Takeaways

  • Production settings: Turn off display_errors, enable log_errors, secure session cookies, and use environment variables for credentials.
  • Deployment: Start with free hosting (InfinityFree) for learning, then graduate to shared hosting or a VPS. Upload files, create the database, update config, and test everything.
  • Environment variables: Keep credentials out of your code using getenv(), .env files, or server configuration. Never commit secrets to Git.
  • Composer: PHP's package manager. Use composer require to install libraries, include vendor/autoload.php once, and use PSR-4 autoloading for your own classes.
  • REST APIs: Return JSON with json_encode(), use proper HTTP status codes, read JSON input from php://input, and add CORS headers for frontend access.
  • Frameworks: Laravel is the recommended next step — it automates routing, validation, CSRF, database queries, and more. Learn vanilla PHP first (you did!), then let frameworks handle the boilerplate.

📚 What You've Accomplished

Over 24 lessons, you've built a complete PHP foundation:

Module What You Learned
1: Getting Started How PHP works, LAMP stack setup, syntax basics, variables, types
2: Core Language Operators, control flow, loops, functions — the building blocks
3: Data Structures Arrays, array functions, strings — manipulating data
4: Web Fundamentals Forms, validation, file handling — processing user input
5: State & OOP Sessions, cookies, classes, inheritance — managing state and structure
6: Database Integration PDO, prepared statements, CRUD, transactions — working with MySQL
7: Real-World Skills File uploads, email, security (SQLi/XSS/CSRF), deployment — production readiness

📚 Additional Resources

🎉🎉🎉 Congratulations! You've Completed PHP Foundations! 🎉🎉🎉

You started with "What is PHP?" and now you can build secure, database-driven web applications with file uploads, email, CSRF protection, and proper deployment. That's a massive achievement.

Every technique in this course — form handling, prepared statements, output escaping, the repository pattern, flash messages, PRG redirects — is used daily in professional PHP development. Whether you move on to Laravel, WordPress, or API development, you have the foundation to understand what's happening under the hood.

Keep building. Keep learning. You've got this.