PHP 8.4+MIT licensed Latest release 0.3

The PHP framework built on PSR standards

Requests, middleware, the container, events, and logs all speak PSR. Choose the router, the view engine, and the ORM; your application code stays the same.

composer create-project crystoline/xpsr my-app
<?php

use App\Http\Controller\TaskController;
use App\Http\Middleware\RequireApiKey;
use Crystoline\Xpsr\Routing\RouteCollection;

return static function (RouteCollection $routes): void {
    $routes->get('/tasks', [TaskController::class, 'index'])
        ->name('tasks.index');

    $routes->get('/tasks/{id}', [TaskController::class, 'show'])
        ->where('id', '\d+')
        ->name('tasks.show');

    $routes->post('/tasks', [TaskController::class, 'store'])
        ->middleware(RequireApiKey::class);
};
<?php

namespace App\Http\Controller;

use App\Tasks\TaskRepository;
use Crystoline\Xpsr\Http\Exception\NotFoundHttpException;
use Crystoline\Xpsr\Http\Response\ResponseBuilder;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

final class TaskController
{
    // The container supplies both by type: no registration needed.
    public function __construct(
        private readonly TaskRepository $tasks,
        private readonly ResponseBuilder $responses,
    ) {}

    public function show(ServerRequestInterface $request): ResponseInterface
    {
        $task = $this->tasks->find((int) $request->getAttribute('id'))
            ?? throw new NotFoundHttpException();

        return $this->responses->json($task);
    }
}
<?php

namespace Tests\Application;

use Tests\TestCase;

final class TaskTest extends TestCase
{
    // Requests run through the whole HTTP kernel, without a web server.
    public function testAMissingTaskIsNotFound(): void
    {
        $response = $this->get('/tasks/99', ['Accept' => 'application/json']);

        self::assertSame(404, $response->getStatusCode());
        self::assertSame(['message' => 'Not Found'], json_decode((string) $response->getBody(), true));
    }
}

Standards first

Built on the interfaces PHP already agrees on

Each part of XPSR implements or consumes a PHP-FIG standard, so libraries written for those standards work without adapters.

What you get

The parts an application needs, none it has to take

The framework stays small. Routers, view engines, ORMs, queues, and mail are separate packages that you install when you need them.

Portable routing

Declare routes once, with groups, constraints, middleware, and names. FastRoute or Sunrise matches them, chosen with one setting.

Read more about portable routing
Next release

Your view engine

Twig by default, or Plates, Latte, or Blade. The same template functions, such as csrf_field() and can(), work in each.

Read more about your view engine
Next release

Sessions and sign-in

Sessions with CSRF protection, session and JWT guards, separate user tables such as admins, and policies for permissions.

Read more about sessions and sign-in
Next release

Work in the background

Queued jobs on Symfony Messenger, email on Symfony Mailer, file storage on Flysystem, and scheduled tasks from one cron entry.

Read more about work in the background

Tests without a server

Application tests send requests through the whole HTTP kernel and run console commands in-process. Freeze the clock, or swap a service.

Read more about tests without a server

Packages

Install only what you use

Every package shares one version number and is released together. Packages marked Next release are on the development branch.

Build your first XPSR API

The tutorial builds a small tasks API with middleware, a console command, and tests, then switches the router without touching a route.