Introduction to Building a CMS Without a Framework

In the world of web development, PHP remains a powerful and versatile language for creating dynamic websites and applications. While frameworks like Laravel and Symfony can streamline the development process, there’s a certain satisfaction in building something from the ground up without relying on pre-existing frameworks. In this article, we’ll delve into the process of creating a simple Content Management System (CMS) using pure PHP, highlighting the key components, and providing step-by-step instructions along with code examples.

Why Build Without a Framework?

Before we dive in, it’s worth asking why you might want to build a CMS without a framework. Here are a few reasons:

  • Learning Experience: Building from scratch helps you understand the underlying mechanics of PHP and web development.
  • Customization: You have complete control over every aspect of your application.
  • Lightweight: No unnecessary overhead from a full-fledged framework.
  • Challenge: It’s a fun and rewarding challenge for developers looking to test their skills.

Setting Up the Project

To start, create a new project directory with the following structure:

project/
├── public/
│   ├── index.php
│   └── .htaccess
├── src/
│   ├── App/
│   │   ├── Lib/
│   │   │   ├── App.php
│   │   │   └── Router.php
│   │   └── Models/
│   │       └── Post.php
│   └── Config/
│       └── config.php
├── composer.json
└── composer.lock

Composer and Autoloading

Even though we’re not using a framework, Composer is still incredibly useful for managing dependencies and setting up autoloading.

Create a composer.json file in your project root:

{
    "require": {
        "monolog/monolog": "1.25.1"
    },
    "autoload": {
        "psr-4": {
            "App\\": "src/App/"
        }
    }
}

Run composer install to set up the autoloader and install any dependencies.

The Front Controller

The front controller is the central entry point for your application. It handles every incoming request and directs it to the appropriate part of your application.

Create public/index.php with the following code:

<?php
declare(strict_types=1);

require __DIR__ . '/vendor/autoload.php';

use App\Lib\App;

App::run();

Routing

Routing is crucial for directing requests to the right parts of your application. Here’s a simple router class to get you started.

Create src/App/Lib/Router.php:

<?php
declare(strict_types=1);

namespace App\Lib;

class Router
{
    private $routes = [];

    public function addRoute(string $method, string $path, callable $callback)
    {
        $this->routes[] = [
            'method' => $method,
            'path' => $path,
            'callback' => $callback,
        ];
    }

    public function dispatch()
    {
        $uri = $_SERVER['REQUEST_URI'];
        $method = $_SERVER['REQUEST_METHOD'];

        foreach ($this->routes as $route) {
            if ($route['method'] === $method && $route['path'] === $uri) {
                return call_user_func($route['callback']);
            }
        }

        http_response_code(404);
        echo 'Page not found';
    }
}

App Bootstrap

Now, let’s create the App class that will bootstrap our application and use the router.

Create src/App/Lib/App.php:

<?php
declare(strict_types=1);

namespace App\Lib;

use App\Lib\Router;

class App
{
    public static function run()
    {
        $router = new Router();

        // Example routes
        $router->addRoute('GET', '/', function () {
            echo 'Welcome to our CMS!';
        });

        $router->addRoute('GET', '/posts', function () {
            // Logic to list posts
            echo 'List of posts';
        });

        $router->dispatch();
    }
}

Database and Models

For simplicity, we’ll use a JSON file as our database. This is not suitable for production but works well for a small example.

Create src/App/Models/Post.php:

<?php
declare(strict_types=1);

namespace App\Models;

class Post
{
    private $id;
    private $title;
    private $body;

    public function __construct(int $id, string $title, string $body)
    {
        $this->id = $id;
        $this->title = $title;
        $this->body = $body;
    }

    public function getId(): int
    {
        return $this->id;
    }

    public function getTitle(): string
    {
        return $this->title;
    }

    public function getBody(): string
    {
        return $this->body;
    }

    public static function loadPosts(): array
    {
        $posts = json_decode(file_get_contents('posts.json'), true);
        return array_map(function ($post) {
            return new self($post['id'], $post['title'], $post['body']);
        }, $posts);
    }

    public static function savePost(Post $post)
    {
        $posts = self::loadPosts();
        $posts[] = [
            'id' => $post->getId(),
            'title' => $post->getTitle(),
            'body' => $post->getBody(),
        ];
        file_put_contents('posts.json', json_encode($posts));
    }
}

Update the App class to include routes for posts:

// In src/App/Lib/App.php

use App\Models\Post;

// ...

$router->addRoute('GET', '/posts', function () {
    $posts = Post::loadPosts();
    foreach ($posts as $post) {
        echo "ID: {$post->getId()}, Title: {$post->getTitle()}, Body: {$post->getBody()}\n";
    }
});

$router->addRoute('POST', '/posts', function () {
    $data = json_decode(file_get_contents('php://input'), true);
    $post = new Post(
        time(), // Simple ID generation
        $data['title'],
        $data['body']
    );
    Post::savePost($post);
    echo 'Post saved successfully';
});

Testing the Application

To test the application, start PHP’s built-in web server:

php -S localhost:8080 -t public/

Navigate to http://localhost:8080/ in your browser to see the welcome message. You can also use tools like Postman or curl to test the POST endpoint.

curl -X POST http://localhost:8080/posts -H 'Content-Type: application/json' -d '{"title":"My Post","body":"This is my post"}'

Dependency Injection

As your application grows, dependency injection becomes crucial for managing dependencies between classes. Here’s a simple example using PHP-DI.

Install PHP-DI via Composer:

composer require php-di/php-di

Create a simple container class:

// In src/App/Lib/Container.php

use DI\Container;

class Container extends Container
{
    public function __construct()
    {
        parent::__construct();

        $this->set('router', function () {
            return new Router();
        });

        $this->set('postModel', function () {
            return new Post(0, '', '');
        });
    }
}

Update the App class to use the container:

// In src/App/Lib/App.php

use App\Lib\Container;

class App
{
    public static function run()
    {
        $container = new Container();

        $router = $container->get('router');

        // Example routes
        $router->addRoute('GET', '/', function () {
            echo 'Welcome to our CMS!';
        });

        // ...

        $router->dispatch();
    }
}

Conclusion

Building a CMS without a framework is a challenging but rewarding experience. It forces you to understand the underlying mechanics of PHP and web development, making you a better developer in the long run. Here’s a quick flowchart to summarize the process:

graph TD A("Create Project Directory") --> B("Set Up Composer") B --> C("Create Front Controller") C --> D("Implement Routing") D --> E("Create Database and Models") E --> F("Implement Dependency Injection") F --> G("Test the Application") G --> B("Deploy and Maintain")

This article has provided a step-by-step guide to building a simple CMS using pure PHP. From setting up the project structure to implementing routing, models, and dependency injection, you now have the tools to create your own custom CMS without relying on a framework. Happy coding