meita / joe
Joe MVC core using JSONBolt and Shams.
Installs: 1
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/meita/joe
Requires
- php: >=8.0
- meita/jsonbolt: ^1.0
- meita/shams: ^1.0
README
Joe is a lightweight MVC core for PHP projects built around:
meita/jsonboltfor the database enginemeita/shamsfor the template engine
Requirements
- PHP 8.0+
- ext-json
- Composer
Installation
composer require meita/joe meita/jsonbolt meita/shams
Project layout (recommended)
your-app/
app/
Controllers/
Models/
components/
public/
index.php
storage/
data/
shams-cache/
views/
home.shams
layouts/
app.shams
Bootstrap
public/index.php
<?php require dirname(__DIR__) . '/vendor/autoload.php'; use Meita\Joe\Application; use Meita\Joe\Database\JsonBoltDatabase; use Meita\Joe\View\ShamsView; use Meita\JsonBolt\Cache\FileCache; use Meita\JsonBolt\Database as JsonBolt; use Meita\Shams\Shams; $app = new Application(dirname(__DIR__)); $shams = new Shams([ 'views_path' => $app->path('views'), 'cache_path' => $app->path('storage/shams-cache'), 'components_path' => $app->path('components'), 'auto_reload' => true, ]); $app->setView(new ShamsView($shams)); $db = new JsonBolt($app->path('storage/data'), [ 'cache' => new FileCache($app->path('storage/cache')), 'cache_ttl' => 300, ]); $app->setDatabase(new JsonBoltDatabase($db)); $app->routes()->get('/', function () use ($app) { return $app->view()->render('home', ['title' => 'Welcome']); }); $app->run();
Routing
$routes = $app->routes(); $routes->get('/', fn () => 'Hello'); $routes->post('/users', [App\Controllers\UserController::class, 'store']); $routes->put('/users/{id}', [App\Controllers\UserController::class, 'update']); $routes->delete('/users/{id}', 'App\Controllers\UserController@destroy'); $routes->any('/health', fn () => ['ok' => true]);
Route groups
$routes->group(['prefix' => '/api'], function ($routes) { $routes->get('/users', [App\Controllers\Api\UserController::class, 'index']); $routes->get('/users/{id}', [App\Controllers\Api\UserController::class, 'show']); });
Route parameters
$routes->get('/users/{id}', function (string $id) { return ['id' => $id]; });
Route middleware
$routes->get('/admin', [App\Controllers\AdminController::class, 'index']) ->middleware(function ($request, $next, $app) { if (!$request->cookie('admin')) { return \Meita\Joe\Http\Response::redirect('/login'); } return $next($request); });
Middleware can also be applied to a group:
$routes->group(['prefix' => '/admin', 'middleware' => [ function ($request, $next, $app) { if (!$request->cookie('admin')) { return \Meita\Joe\Http\Response::redirect('/login'); } return $next($request); }, ]], function ($routes) { $routes->get('/users', [App\Controllers\AdminController::class, 'users']); });
Handler return values
Routes and controllers may return:
Meita\Joe\Http\Response(sent as-is)array(JSON response)stringor any scalar (HTML response)
Requests
use Meita\Joe\Http\Request; $routes->post('/users', function (Request $request) { $email = $request->input('email'); $payload = $request->json(); return ['email' => $email, 'payload' => $payload]; });
Available helpers:
method(),path()query(),body(),all(),input($key, $default)header($name),headers()cookie($name),cookies()files()raw(),json()attributes(),attribute($key, $default)for route params
Method override is supported via X-HTTP-Method-Override header or _method
in form/query data (POST requests).
Responses
use Meita\Joe\Http\Response; return Response::html('<h1>Hello</h1>', 200); return Response::json(['ok' => true], 200); return Response::redirect('/login');
Controllers
<?php namespace App\Controllers; use Meita\Joe\Http\Controller; use Meita\Joe\Http\Response; final class HomeController extends Controller { public function index(): Response { return $this->view('home', ['title' => 'Hello']); } public function profile(): Response { $user = $this->db()->collection('users')->find(1); return $this->json($user ?? []); } }
$app->routes()->get('/', [App\Controllers\HomeController::class, 'index']);
Views (Shams)
Joe uses Shams as the view engine. Configure it and pass the adapter:
use Meita\Joe\View\ShamsView; use Meita\Shams\Shams; $shams = new Shams([ 'views_path' => $app->path('views'), 'cache_path' => $app->path('storage/shams-cache'), 'components_path' => $app->path('components'), 'asset_url' => '/assets', ]); $app->setView(new ShamsView($shams));
Example template views/home.shams:
@extends('layouts.app')
@section('title', $title)
@section('content')
<h1>{{ $title }}</h1>
@endsection
Database (JSONBolt)
Joe wraps JSONBolt via JsonBoltDatabase:
use Meita\Joe\Database\JsonBoltDatabase; use Meita\JsonBolt\Database as JsonBolt; $db = new JsonBolt($app->path('storage/data')); $app->setDatabase(new JsonBoltDatabase($db));
Collections are used like JSONBolt:
$users = $app->db()->collection('users'); $user = $users->insert(['name' => 'Sara', 'email' => 'sara@example.com']); $found = $users->find($user['id']);
Models
Create a model per collection:
<?php namespace App\Models; use Meita\Joe\Database\Model; final class User extends Model { public static function collection(): string { return 'users'; } }
$users = new App\Models\User($app->db()); $users->create(['name' => 'Mona']); $users->where('active', true)->orderBy('name')->get();
Middleware
Middleware runs before the router and can short-circuit the request:
use Meita\Joe\Application; use Meita\Joe\Http\Request; $app->addMiddleware(function (Request $request, callable $next, Application $app) { if ($request->path() === '/health') { return ['ok' => true]; } return $next($request); });
Dependency injection
The container can resolve class-hinted arguments in route handlers:
use Meita\Joe\Container\Container; use Meita\Joe\Http\Request; $app->container()->bind(MyService::class, fn (Container $c) => new MyService()); $app->routes()->get('/ping', function (Request $request, MyService $service) { return ['pong' => true]; });
Configuration
$app->config()->set('app.env', 'local'); $env = $app->config()->get('app.env', 'production');
Error handling
Joe keeps error handling simple. Add a top-level middleware to convert exceptions:
use Meita\Joe\Http\Response; $app->addMiddleware(function ($request, $next) { try { return $next($request); } catch (Throwable $e) { return Response::html('Server Error', 500); } });
Deployment
- Point your web server document root to
public/. - Ensure
storage/is writable.
Optional .htaccess for Apache:
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ index.php [QSA,L]
License
MIT