Getting Started

Everything you need to install the Melodic PHP framework, set up a project, and bootstrap your first application.

Installation

Install Melodic via Composer:

composer require melodicdev/framework

Requirements

DependencyVersionPurpose
PHP8.2+Language runtime — enums, readonly properties, constructor promotion
PDO extensionanyDatabase access via DbContext
firebase/php-jwt^7.0JWT encoding, decoding, and validation

Note: The firebase/php-jwt package is pulled in automatically as a Composer dependency. You do not need to require it separately.

Verify your PHP version from the command line:

php -v
# PHP 8.2.0 (cli) ...

Confirm the PDO extension is loaded:

php -m | grep -i pdo
# PDO
# pdo_mysql
# pdo_sqlite

Quick Start with Scaffolding

The fastest way to start a new project is with the melodic CLI:

# Create a new project (MVC + API by default)
vendor/bin/melodic make:project my-app
cd my-app && composer install
php -S localhost:8080 -t public

# Or create API-only or MVC-only projects
vendor/bin/melodic make:project my-api --type=api
vendor/bin/melodic make:project my-site --type=mvc

The default project type is full — it includes both MVC views and API routing, which is what most real-world applications need. You can also create API-only or MVC-only projects if you prefer a narrower starting point.

Tip: See the Console documentation for full details on make:project, make:entity, and other CLI commands.

Project Structure

A Melodic application follows this canonical directory layout. This is what make:project generates by default:

my-app/
│── composer.json               ← PSR-4: App\ → src/
│── config/
│   │── config.json              ← Application configuration
│   └── config.dev.json          ← Local overrides (gitignored)
│── public/
│   │── index.php                ← HTTP entry point (document root)
│   └── .htaccess                ← Apache rewrite rules
│── bin/
│   └── console                  ← CLI entry point
│── src/
│   │── Controllers/             ← ApiController and MvcController subclasses
│   │── Services/                ← Service subclasses (business logic)
│   │── DTO/                     ← Models extending Melodic\Data\Model
│   │── Data/
│   │   └── {Entity}/
│   │       │── Queries/         ← QueryInterface implementations
│   │       └── Commands/        ← CommandInterface implementations
│   │── Middleware/              ← Custom middleware classes
│   └── Providers/
│       └── AppServiceProvider.php
│── views/
│   │── layouts/
│   │   └── main.phtml           ← Base HTML layout
│   └── home/
│       └── index.phtml          ← Home page template
│── storage/
│   │── cache/
│   └── logs/
│── tests/
└── vendor/

API and MVC controllers coexist in the same src/Controllers/ directory. Use route groups to organize them:

$app->routes(function ($router) {
    // MVC routes
    $router->get('/', HomeController::class, 'index');

    // API routes
    $router->group('/api', function ($router) {
        $router->apiResource('/users', UserController::class);
    });
});

Project Types

TypeFlagDescription
Full (default)--type=full or omitMVC views + API routing — recommended for most apps
API only--type=apiNo views directory, API-only index.php
MVC only--type=mvcViews + HomeController, MVC-only index.php

Naming Conventions

TypeLocationNaming PatternExample
DTO / Modelsrc/DTO/{Entity}ModelUserModel
Querysrc/Data/{Entity}/Queries/Get{Entity}ByIdQueryGetUserByIdQuery
Commandsrc/Data/{Entity}/Commands/Create{Entity}CommandCreateUserCommand
Servicesrc/Services/{Entity}ServiceUserService
Controllersrc/Controllers/{Entity}ControllerUserController
Providersrc/Providers/{Name}ServiceProviderAppServiceProvider

PSR-4 Autoloading

Register your application namespace in composer.json. The framework itself uses the Melodic\ namespace; your application code lives under its own namespace:

{
    "autoload": {
        "psr-4": {
            "App\\": "src/"
        }
    }
}

After editing composer.json, regenerate the autoloader:

composer dump-autoload

Tip: Point your web server's document root to the public/ directory. This keeps your configuration files, source code, and views outside the publicly accessible path.

Application Bootstrap

The entry point for every Melodic application is public/index.php. This file creates an Application instance and configures it step by step. Here is a detailed walkthrough of each stage.

1. Require the Autoloader

<?php

declare(strict_types=1);

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

use Melodic\Core\Application;

The Composer autoloader handles class loading for both the framework (Melodic\) and your application namespace.

2. Create the Application

$app = new Application(dirname(__DIR__));

The constructor accepts a base path — the root directory of your project. Since index.php lives in public/, use dirname(__DIR__) to point to the project root. All relative paths for configuration and views are resolved from this base path.

Note: If your entry point is at the project root rather than in a public/ subdirectory, use new Application(__DIR__) instead.

3. Load Configuration

$app->loadEnvironmentConfig();

This single call loads configuration files from config/ in the correct order: config.json (base), then config.{APP_ENV}.json (environment overrides), then config.dev.json (local developer overrides, gitignored). The APP_ENV environment variable controls which environment file is loaded and defaults to dev. See the Configuration docs for full details on the loading order and environment management.

4. Register Service Providers

use Melodic\Security\SecurityServiceProvider;
use Melodic\Log\LoggingServiceProvider;
use Melodic\Event\EventServiceProvider;
use Melodic\Cache\CacheServiceProvider;
use Melodic\Session\SessionServiceProvider;

$app->register(new LoggingServiceProvider());
$app->register(new EventServiceProvider());
$app->register(new CacheServiceProvider());
$app->register(new SessionServiceProvider());
$app->register(new SecurityServiceProvider());

Service providers are modular packages that register bindings in the DI container. The framework ships with several built-in providers:

  • LoggingServiceProvider — registers the logger and configures file-based log output
  • EventServiceProvider — registers the event dispatcher for decoupled event handling
  • CacheServiceProvider — registers the cache manager with file-based caching support
  • SessionServiceProvider — registers the session manager for session-based state
  • SecurityServiceProvider — wires up JWT validation, authentication middleware, OAuth/OIDC providers, and session management

You can create your own providers by extending Melodic\DI\ServiceProvider.

5. Register Services

use Melodic\Data\DbContext;
use Melodic\Data\DbContextInterface;

$app->services(function ($container) use ($app) {
    // Singleton: one DbContext instance shared across the request
    $container->singleton(DbContextInterface::class, function () use ($app) {
        $pdo = new PDO(
            $app->config('database.dsn'),
            $app->config('database.username'),
            $app->config('database.password'),
        );
        $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        return new DbContext($pdo);
    });

    // Interface binding: Container resolves UserServiceInterface to UserService
    $container->bind(UserServiceInterface::class, UserService::class);
});

The callback receives the DI Container. Use it to register singletons (shared instances), interface-to-class bindings, and factory functions. The container supports auto-wiring, so any class whose constructor parameters are type-hinted with registered interfaces will be resolved automatically.

6. Add Middleware

use Melodic\Http\Middleware\CorsMiddleware;
use Melodic\Http\Middleware\JsonBodyParserMiddleware;

$corsConfig = $app->config('cors') ?? [];
$app->addMiddleware(new CorsMiddleware($corsConfig));
$app->addMiddleware(new JsonBodyParserMiddleware());

Global middleware is executed on every request, in the order it is added. Middleware added first runs first on the way in and last on the way out (onion model).

7. Define Routes

You can define routes inline:

$app->routes(function ($router) {
    $router->get('/', HomeController::class, 'index');
    $router->apiResource('/api/users', UserController::class);
});

Or load them from a separate file that returns a callable:

// config/routes.php
return function (Router $router): void {
    $router->get('/', HomeController::class, 'index');
};

// public/index.php
$app->routes(require dirname(__DIR__) . '/config/routes.php');

8. Run

$app->run();

This captures the current HTTP request from PHP superglobals, boots any registered service providers, builds the middleware pipeline, dispatches the request through it, and sends the response to the client.

Application Class Methods

The Melodic\Core\Application class is the central orchestrator. Here is the full reference of its public API.

MethodDescription
__construct(string $basePath) Creates the application. The base path is used to resolve relative config and view paths. Also initializes the DI container, router, and configuration, and registers them as container instances.
loadEnvironmentConfig(string $configDir = 'config'): self Loads configuration files from the given directory in order: config.json, then config.{APP_ENV}.json, then config.dev.json. Sets app.environment in config. Returns $this for chaining.
loadConfig(string $path): self Loads a single JSON configuration file. Relative paths are resolved from the base path. Can be called multiple times — subsequent calls deep-merge into existing config. Returns $this for chaining.
getEnvironment(): string Returns the current environment name (e.g., 'dev', 'qa', 'pd'). Reads from app.environment in config, defaults to 'dev'.
config(?string $key = null, mixed $default = null): mixed Reads a configuration value using dot-notation (e.g., 'database.dsn'). If $key is null, returns the entire Configuration object. Returns $default if the key is not found.
getBasePath(): string Returns the base path passed to the constructor.
register(ServiceProvider $provider): self Registers a service provider. Calls the provider's register() method immediately with the container. The provider's boot() method is called later when run() is invoked. Returns $this for chaining.
services(callable $callback): self Invokes the callback with the DI Container, allowing you to register bindings, singletons, and factories. Returns $this for chaining.
addMiddleware(MiddlewareInterface $middleware): self Adds a global middleware to the pipeline. Middleware runs in the order added. Returns $this for chaining.
routes(callable $callback): self Invokes the callback with the Router, allowing you to register routes, groups, and API resources. Returns $this for chaining.
run(?Request $request = null): void Boots all service providers, captures the HTTP request (or uses the one provided), builds the middleware pipeline with the routing middleware as the final handler, dispatches the request, and sends the response.
getContainer(): Container Returns the DI container instance.
getRouter(): Router Returns the router instance.
getConfiguration(): Configuration Returns the Configuration instance.

Tip: Most Application methods return $this, so you can chain them: $app->loadEnvironmentConfig()->register(new SecurityServiceProvider())->run().

Complete Minimal Example

Here is a fully working hello-world application in three files.

composer.json

{
    "require": {
        "melodicdev/framework": "*"
    },
    "autoload": {
        "psr-4": {
            "App\\": "src/"
        }
    }
}

config/config.json

{
    "app": {
        "name": "Hello World"
    }
}

src/Controllers/HelloController.php

<?php

declare(strict_types=1);

namespace App\Controllers;

use Melodic\Controller\Controller;
use Melodic\Http\Response;

class HelloController extends Controller
{
    public function index(): Response
    {
        return $this->json([
            'message' => 'Hello from Melodic!',
        ]);
    }

    public function show(int $id): Response
    {
        return $this->json([
            'message' => "Hello, user #{$id}!",
        ]);
    }
}

public/index.php

<?php

declare(strict_types=1);

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

use App\Controllers\HelloController;
use Melodic\Core\Application;
use Melodic\Http\Middleware\JsonBodyParserMiddleware;

$app = new Application(dirname(__DIR__));
$app->loadEnvironmentConfig();

$app->addMiddleware(new JsonBodyParserMiddleware());

$app->routes(function ($router) {
    $router->get('/', HelloController::class, 'index');
    $router->get('/hello/{id}', HelloController::class, 'show');
});

$app->run();

Run It

composer install
php -S localhost:8080 -t public

Then visit http://localhost:8080/ to see the JSON response, or http://localhost:8080/hello/42 to see a parameterized route in action.

Important: The built-in PHP development server is for local development only. For production, use a proper web server such as Nginx or Apache with PHP-FPM, and point the document root to the public/ directory.