Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

192 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Piper

tests php license

This repository is the framework itself. To build something with it, start from amsrafid/piper by running composer create-project amsrafid/piper my-api. That gives you the directory layout, the config files and the console script, and pulls this in as a dependency. Everything below is what you get once it's there.

A small PHP framework for building APIs. I wrote it in 2022, on my own, to learn what a framework has to do. Routing, a container, middleware, migrations, a token guard and a code generator, each one built rather than read about.

It still runs. In 2026 I went back through it, fixed what was broken and added attribute routing. The commit history has both halves in it.

How it differs from a smaller Laravel: you don't have to declare your routes. A controller method is reachable as soon as you write it, and you declare a route only when you want something the convention can't give you.

class ShopController extends Controller
{
    public function index() { }                 // GET /shop/index, nothing declared

    #[Get('/shop/{slug}')]                      // declared, because a slug is not a convention
    public function bySlug($slug) { }
}

Requirements

PHP 8.0, 8.1, 8.2 or 8.3. The composer.json says >=8.0 <8.4 and means it. On 8.4 the deprecations inside illuminate/database 8.x fail, not Piper's own code.

Routing

Three ways to reach a controller method. They work together, and later ones win.

Convention, the default

The URI maps to a controller and a method. App\Http\Controllers\ShopController::index() answers /shop/index, and numeric segments after that become arguments.

Nothing to register. This is what you get by writing a method.

The convention has one limit worth knowing: a segment only becomes an argument if it is numeric. /shop/42 reaches index(42), but /shop/blue-cap looks for a Shop\BlueCapController. That is what attributes are for.

Attributes, when the convention is not enough

use Piper\Routing\Attributes\Get;
use Piper\Routing\Attributes\Middleware;
use Piper\Routing\Attributes\Post;
use Piper\Routing\Attributes\Prefix;
use Piper\Routing\Attributes\Route;

#[Prefix('/api/v2')]
#[Middleware('auth:api')]
class OrderController extends Controller
{
    #[Get]
    public function index() { }                        // GET /api/v2/order/index

    #[Get('/orders/{slug}/lines/{line}')]
    public function lines($line, $slug) { }            // parameters arrive by name, in any order

    #[Get('/orders/{id:\d+}')]
    public function show($id) { }                      // constrained

    #[Get('/orders/page/{number?}')]
    public function page($number = 1) { }              // optional

    #[Post('/orders')]
    #[Middleware('throttle')]
    public function store(Request $request) { }        // type hints still come from the container

    #[Route('/health', methods: ['GET', 'HEAD'])]
    public function health() { }
}

#[Get] #[Post] #[Put] #[Patch] #[Delete] #[Options] #[Head] and the generic #[Route] all take an optional path. Leave the path out and the convention still supplies it. All of them are repeatable, so one method can answer more than one path.

Placeholders: {id}, {id?}, {id:\d+}, {id?:\d+}. The ? goes before the :, because {id:\d+?} would be ambiguous. ? is a regex quantifier too.

#[Prefix] on the class prefixes every route in it, including the convention-derived ones. Note that it stacks with the namespace. A controller in App\Http\Controllers\Api that also declares #[Prefix('/api/v2')] ends up under /api/v2/api/..., so put the controller where you want it or set the whole path explicitly.

#[Middleware] works on the class and on the method. It merges with the kernel's global and prefix middleware instead of replacing it.

The constructor, from 2022

public function __construct()
{
    $this->post('store');                    // restrict a method to a verb
    $this->middleware('auth');               // controller-wide
    $this->middleware(['auth:api' => 'store']);
}

Still supported, still works. An attribute on the same method wins.

Middleware

Global and prefix middleware live in the application's app/Http/Kernel.php:

protected $middleware = [
    \Piper\Foundation\Middleware\KeyValidation::class,
];

protected $prefixMiddleware = [
    '/api' => [\Piper\Authentication\Middleware\ApiDefault::class],
];

protected $routeMiddleware = [
    'auth:api' => \App\Http\Middleware\AuthApi::class,
];

A middleware receives the request and the next handler. Returning a response instead of calling $next stops the request there.

Console

php amsrafid make:controller Shop
php amsrafid make:middleware CheckAge
php amsrafid make:model Order
php amsrafid make:migration create_orders_table
php amsrafid migrate
php amsrafid migrate:rollback --file=create_orders_table
php amsrafid migration:status
php amsrafid make:service-provider Payment
php amsrafid storage:link

Database

Eloquent, through illuminate/database. Models extend Piper\Database\Model, migrations are plain classes with up() and down(), and the connection is configured in config/database.php and env.xml.

Authentication

A JWT guard with pluggable drivers and providers, configured in config/auth.php. $this->auth() inside a controller gives you the guard.

What it does not do

Worth knowing before you pick it up:

  • No view layer. It returns API responses. The 2022 README said a view layer was coming in the next version. It never got written, and this is an API framework.
  • No route cache yet. The scanner reflects over your controllers on each request that reaches the dispatcher. That is a handful of files and no database work, but on a large application you will want route:cache. It is the next thing on the list.
  • No route:list yet. The table exists and is complete. The command that prints it comes with the cache.
  • Apache assumed. The framework itself reads nothing Apache-specific, but the starter application routes through a public/.htaccess, so nginx with FPM needs the same rewrite written by hand.
  • One application per install. No multi-tenancy, no package discovery, no events.

Tests

composer install
composer test

The suite is small on purpose. It covers the parts that break quietly:

File What it holds
BootTest the application boots, and every binding the framework makes for itself resolves
AttributeRouteTest the scanner, the table and the matcher, through a real request
ConventionRouteTest a controller that declares nothing is still routed by convention
ErrorResponseTest with debug off, only deliberate 4xx exceptions report a message

Each test boots a fixture application under tests/Fixtures/App, so the boot path is the same one a front controller takes. CI runs the suite on 8.0, 8.1, 8.2 and 8.3.

Not covered yet: migrations, an authenticated request and the console commands. Those need a database and an HTTP server rather than a fixture, and they are next.

Contributing

Issues and pull requests are welcome. Run composer test before opening one. The framework is expected to boot, serve and run its console on 8.0 through 8.3.

Author

A. M. Sadman Rafid, amsrafid.com · github.com/amsrafid

Security

If you find a security problem, please email amsrafid@gmail.com rather than opening a public issue.

License

MIT.

About

Piper framework core

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages