monkeyscloud / monkeyslegion-skeleton
Starter project for the MonkeysLegion framework
Package info
github.com/MonkeysCloud/MonkeysLegion-Skeleton
Type:project
pkg:composer/monkeyscloud/monkeyslegion-skeleton
Requires
- php: ^8.4
- laminas/laminas-diactoros: ^3.8
- monkeyscloud/monkeyslegion: ^2.0
- monkeyscloud/monkeyslegion-cli: ^2.0.1
- monolog/monolog: ^3.10
Requires (Dev)
- http-interop/http-factory-tests: ^2.2
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^11.0
- squizlabs/php_codesniffer: ^4.0
- 2.0.x-dev
- 2.0.0
- 1.0.x-dev
- dev-main / 1.0.x-dev
- 1.0.17
- 1.0.16
- 1.0.15
- 1.0.14
- 1.0.13
- 1.0.12
- 1.0.11
- 1.0.10
- 1.0.9
- 1.0.8
- 1.0.7
- 1.0.6
- 1.0.5
- 1.0.4
- 1.0.3
- 1.0.2
- 1.0.1
- 1.0.0
- dev-fix/database-fallback-sqlite
- dev-feature/default-session-file-driver
- dev-register/session
- dev-copilot/sub-pr-8
- dev-fix/restore-bin-scripts
- dev-user-auth-migration
- dev-dev
This package is auto-updated.
Last update: 2026-04-28 05:06:43 UTC
README
Production-ready PHP 8.4 skeleton for building web apps & APIs with the MonkeysLegion framework v2.
Built on attribute-first routing, property hooks, asymmetric visibility, and a zero-magic PSR-15 pipeline.
β¨ What's New in v2
| Feature | v1 | v2 |
|---|---|---|
| Entry point | HttpBootstrap::run() |
Application::create()->run() |
| Entity properties | Getters/setters | PHP 8.4 property hooks |
| Visibility | Public/private | public private(set) asymmetric |
| Configuration | .php arrays |
.mlc typed config |
| Routing | Manual registration | #[Route] attributes |
| Auth | Manual middleware | #[Authenticated], #[RequiresRole] |
| Rate limiting | Custom | #[Throttle(max: 60, per: 1)] |
| Events | Manual dispatch | #[Listener] auto-discovery |
| DI | Config-first | #[Singleton], #[Provider] |
| PHPStan | Level 8 | Level 9 |
| Tests | 12 tests | 139 tests, 289 assertions |
β¨ Features Overview
| Category | Features |
|---|---|
| HTTP Stack | PSR-7/15 compliant, middleware pipeline, SAPI emitter |
| Routing | Attribute-based v2, auto-discovery, constraints, caching |
| Dependency Injection | PSR-11 container with #[Singleton], #[Provider] |
| Database | Native PDO MySQL 8.4, Query Builder, Micro-ORM |
| Authentication | JWT, RBAC, 2FA, OAuth, API keys |
| API Documentation | Live OpenAPI 3.1 & Swagger UI |
| Validation | DTO binding with attribute constraints |
| Rate Limiting | #[Throttle] attribute, sliding-window (IP + User) |
| Templating | MLView with components, slots, caching |
| CLI | Migrations, cache, key-gen, scaffolding, Tinker REPL |
| Files | Multi-driver storage, image processing, chunked uploads |
| I18n | Full internationalization & localization support |
| Telemetry | Prometheus metrics, distributed tracing, PSR-3 logging |
| SMTP, Markdown templates, DKIM support | |
| Caching | Multiple drivers (File, Redis, Memcached) |
π Quick Start
composer create-project monkeyscloud/monkeyslegion-skeleton my-app cd my-app cp .env.example .env php ml key:generate composer serve # β http://127.0.0.1:8000
π Project Structure
my-app/
ββ app/
β ββ Controller/ # Attribute-routed controllers
β β ββ Api/ # API controllers (UserController, PostController, AuthController)
β ββ Dto/ # Request DTOs with validation attributes
β ββ Entity/ # Entities with PHP 8.4 property hooks
β ββ Enum/ # Backed enums with business logic
β ββ Event/ # Domain events (final readonly)
β ββ Job/ # Queue jobs (ShouldQueue)
β ββ Listener/ # Event listeners (#[Listener])
β ββ Middleware/ # PSR-15 middleware
β ββ Policy/ # Authorization policies
β ββ Providers/ # Service providers (#[Provider])
β ββ Repository/ # EntityRepository<T> extensions
β ββ Resource/ # JSON:API resource transformers
β ββ Service/ # Business logic (#[Singleton])
ββ config/
β ββ app.php # DI container bindings (only PHP config file)
β ββ app.mlc # Application settings
β ββ database.mlc # Database connection
β ββ auth.mlc # JWT, guards, 2FA
β ββ cache.mlc # Cache drivers
β ββ cors.mlc # CORS policy
β ββ logging.mlc # Log channels
β ββ mail.mlc # SMTP/mailer
β ββ middleware.mlc # Middleware pipeline
β ββ queue.mlc # Queue drivers
β ββ session.mlc # Session config
ββ public/index.php # Application::create()->run()
ββ bootstrap.php # Application::create()->boot()
ββ ml # CLI entry point
ββ src/helpers.php # Global helper functions (base_path, asset, csrf, auth)
ββ resources/
β ββ views/ # MLView templates & components
ββ storage/ # File uploads, logs
ββ var/
β ββ cache/ # Compiled templates, route cache
β ββ migrations/ # Auto-generated SQL
ββ tests/
β ββ Unit/ # 100+ unit tests
β ββ Integration/ # Integration tests with DI container
β ββ Feature/ # Full HTTP pipeline tests
β ββ Performance/ # Benchmark suite (11 benchmarks)
ββ phpunit.xml
ββ phpstan.neon # Level 9
ββ composer.json
ποΈ v2 Architecture
Entry Point
// public/index.php β the entire entry point <?php declare(strict_types=1); require dirname(__DIR__) . '/vendor/autoload.php'; MonkeysLegion\Framework\Application::create( basePath: dirname(__DIR__), )->run();
Entities (PHP 8.4 Property Hooks + Asymmetric Visibility)
use MonkeysLegion\Entity\Attributes\Entity; use MonkeysLegion\Entity\Attributes\Field; use MonkeysLegion\Entity\Attributes\Id; use MonkeysLegion\Entity\Attributes\Timestamps; use MonkeysLegion\Auth\Contract\AuthenticatableInterface; use MonkeysLegion\Auth\Contract\HasRolesInterface; #[Entity(table: 'users')] #[Timestamps] final class User implements AuthenticatableInterface, HasRolesInterface { // Auto-increment ID β readable by all, writable only by the ORM #[Id] #[Field(type: 'unsignedBigInt', autoIncrement: true)] public private(set) int $id; // Property hook: auto-lowercase and trim on set #[Field(type: 'string', length: 255, unique: true)] public string $email { set(string $value) { $this->email = strtolower(trim($value)); } } // Property hook: validation on set #[Field(type: 'string', length: 100)] public string $name { set(string $value) { if (strlen($value) === 0) { throw new \InvalidArgumentException('Name cannot be empty'); } $this->name = $value; } } #[Field(type: 'string', length: 255)] public string $password_hash; #[Field(type: 'datetime', nullable: true)] public ?\DateTimeImmutable $email_verified_at = null; #[Field(type: 'integer')] public int $token_version = 1; // Computed properties β no backing field, no DB column public string $displayName { get => "{$this->name} <{$this->email}>"; } public bool $isVerified { get => $this->email_verified_at !== null; } // RBAC interface implementation /** @var list<string> */ protected array $roles = []; /** @var list<string> */ protected array $permissions = []; public function getRoles(): array { return $this->roles; } public function hasRole(string $role): bool { return in_array($role, $this->roles, true); } public function hasPermission(string $permission): bool { foreach ($this->permissions as $p) { if ($p === '*' || $p === $permission) return true; if (str_ends_with($p, '.*') && str_starts_with($permission, rtrim($p, '.*'))) return true; } return false; } // Auth interface public function getAuthIdentifier(): int|string { return $this->id; } public function getAuthPassword(): string { return $this->password_hash; } public function bumpTokenVersion(): void { $this->token_version++; } // Lifecycle helpers public function markEmailVerified(): void { $this->email_verified_at = new \DateTimeImmutable(); } }
Services (#[Singleton] + PSR-14 Events)
use MonkeysLegion\DI\Attributes\Singleton; use Psr\EventDispatcher\EventDispatcherInterface; use Psr\Log\LoggerInterface; #[Singleton] final class UserService { public function __construct( private readonly UserRepository $users, private readonly EventDispatcherInterface $events, private readonly LoggerInterface $logger, ) {} public function createUser(CreateUserRequest $dto): User { $user = new User(); $user->email = $dto->email; $user->name = $dto->name; $user->password_hash = password_hash($dto->password, PASSWORD_DEFAULT); $this->users->persist($user); $this->events->dispatch(new UserCreated($user)); $this->logger->info('User created', ['email' => $user->email]); return $user; } public function findUser(int $id): ?User { return $this->users->find($id); } public function deleteUser(int $id): void { $this->users->delete($id); $this->logger->info('User deleted', ['id' => $id]); } }
Controllers (Attribute Routing + Authorization)
use MonkeysLegion\Router\Attributes\Route; use MonkeysLegion\Router\Attributes\RoutePrefix; use MonkeysLegion\Router\Attributes\Middleware; use MonkeysLegion\Auth\Attribute\Authenticated; use MonkeysLegion\Auth\Attribute\RequiresRole; use MonkeysLegion\Http\Message\Response; #[RoutePrefix('/api/v2/users')] #[Middleware(['cors', 'throttle:60,1'])] #[Authenticated] final class UserController { public function __construct( private readonly UserService $service, private readonly UserRepository $users, ) {} #[Route('GET', '/', name: 'users.index', summary: 'List users', tags: ['Users'])] public function index(ServerRequestInterface $request): Response { return UserResource::collection($this->users->findActiveUsers()); } #[Route('GET', '/{id:\d+}', name: 'users.show', summary: 'Get user by ID')] public function show(ServerRequestInterface $request, string $id): Response { $user = $this->users->findOrFail((int) $id); return UserResource::make($user); } #[Route('POST', '/', name: 'users.create')] #[RequiresRole('admin')] public function create(CreateUserRequest $dto): Response { $user = $this->service->createUser($dto); return UserResource::make($user, 201); } #[Route('PUT', '/{id:\d+}', name: 'users.update')] #[RequiresRole('admin')] public function update(UpdateUserRequest $dto, string $id): Response { $user = $this->service->updateUser((int) $id, $dto); return UserResource::make($user); } #[Route('DELETE', '/{id:\d+}', name: 'users.destroy')] #[RequiresRole('admin')] public function destroy(string $id): Response { $this->service->deleteUser((int) $id); return Response::noContent(); } }
DTOs (Validation Attributes)
use MonkeysLegion\Validation\Attributes\NotBlank; use MonkeysLegion\Validation\Attributes\Email; use MonkeysLegion\Validation\Attributes\Length; final readonly class CreateUserRequest { public function __construct( #[NotBlank] #[Email] public string $email, #[NotBlank] #[Length(min: 2, max: 100)] public string $name, #[NotBlank] #[Length(min: 8, max: 64)] public string $password, ) {} } // Partial update DTO β all nullable final readonly class UpdateUserRequest { public function __construct( #[Email] public ?string $email = null, #[Length(min: 2, max: 100)] public ?string $name = null, #[Length(min: 8, max: 64)] public ?string $password = null, ) {} }
Available Validation Constraints:
#[NotBlank]β Value cannot be empty#[Email]β Valid email format#[Length(min, max)]β String length range#[Range(min, max)]β Numeric range#[Pattern(regex)]β Regex pattern match#[Url]β Valid URL format#[UuidV4]β Valid UUIDv4 format
Validation Error Response (400):
{
"errors": [
{ "field": "email", "message": "Value must be a valid e-mail." },
{ "field": "password", "message": "Length constraint violated." }
]
}
JSON:API Resources
final class UserResource { public static function toArray(User $user): array { return [ 'id' => $user->id, 'type' => 'users', 'attributes' => [ 'email' => $user->email, 'name' => $user->name, 'is_verified' => $user->isVerified, 'created_at' => $user->created_at->format('c'), 'updated_at' => $user->updated_at->format('c'), ], ]; } public static function make(User $user, int $status = 200): Response { return Response::json(['data' => self::toArray($user)], $status); } public static function collection(array $users): Response { return Response::json([ 'data' => array_map(self::toArray(...), $users), 'meta' => ['total' => count($users)], ]); } }
Events & Listeners
// Domain event β final readonly, automatically timestamped final readonly class UserCreated { public function __construct( public User $user, public \DateTimeImmutable $createdAt = new \DateTimeImmutable(), ) {} } // Listener β auto-discovered via #[Listener] attribute use MonkeysLegion\Events\Attribute\Listener; #[Listener(UserCreated::class)] final class SendWelcomeEmail { public function __construct(private readonly LoggerInterface $logger) {} public function __invoke(UserCreated $event): void { $this->logger->info('Queuing welcome email', [ 'user_id' => $event->user->id, 'email' => $event->user->email, ]); // Dispatch SendWelcomeEmailJob to queue... } }
Queue Jobs
use MonkeysLegion\Queue\Contracts\ShouldQueue; final class SendWelcomeEmailJob implements ShouldQueue { public function __construct( private readonly int $userId, ) {} public function handle(UserRepository $users, LoggerInterface $logger): void { $user = $users->find($this->userId); if ($user === null) { $logger->warning('SendWelcomeEmail: user not found', ['user_id' => $this->userId]); return; } // Send the actual email via Mailer... $logger->info('Welcome email sent', ['user_id' => $this->userId, 'email' => $user->email]); } public function failed(\Throwable $e): void { // Handle failure (retry, DLQ, notify, etc.) } }
Authorization Policies
final class PostPolicy { public function update(User $user, Post $post): bool { return $user->id === $post->author->id || $user->hasRole('admin'); } public function delete(User $user, Post $post): bool { return $user->hasRole('admin'); } public function publish(User $user, Post $post): bool { return $user->id === $post->author->id || $user->hasRole('admin') || $user->hasRole('editor'); } }
Backed Enums
enum OrderStatus: string { case Pending = 'pending'; case Confirmed = 'confirmed'; case Processing = 'processing'; case Shipped = 'shipped'; case Delivered = 'delivered'; case Cancelled = 'cancelled'; public function isFinal(): bool { return match ($this) { self::Delivered, self::Cancelled => true, default => false, }; } public function label(): string { return match ($this) { self::Pending => 'Pending Review', self::Shipped => 'In Transit', default => $this->name, }; } public function color(): string { return match ($this) { self::Pending => '#f59e0b', self::Confirmed => '#3b82f6', self::Processing => '#8b5cf6', self::Shipped => '#06b6d4', self::Delivered => '#10b981', self::Cancelled => '#ef4444', }; } /** @return list<self> */ public static function active(): array { return array_filter(self::cases(), fn(self $s) => !$s->isFinal()); } } enum UserRole: string { case Admin = 'admin'; case Editor = 'editor'; case User = 'user'; /** @return list<string> */ public function permissions(): array { return match ($this) { self::Admin => ['*'], self::Editor => ['posts.*', 'comments.*'], self::User => ['posts.view', 'comments.view', 'comments.create'], }; } public function isAdmin(): bool { return $this === self::Admin; } }
PSR-15 Middleware
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; final class TimingMiddleware implements MiddlewareInterface { public function process( ServerRequestInterface $request, RequestHandlerInterface $handler, ): ResponseInterface { $start = hrtime(true); $response = $handler->handle($request); $durationMs = (hrtime(true) - $start) / 1e6; return $response->withHeader( 'Server-Timing', sprintf('total;dur=%.2f', $durationMs), ); } }
Repositories
use MonkeysLegion\Query\Repository\EntityRepository; /** * @extends EntityRepository<User> */ final class UserRepository extends EntityRepository { public function findByEmail(string $email): ?User { return $this->findOneBy(['email' => $email]); } /** @return list<User> */ public function findActiveUsers(): array { return $this->findBy( criteria: ['status' => 'active'], orderBy: ['created_at' => 'DESC'], ); } } // EntityRepository<T> provides: // - find(int $id): ?T // - findOrFail(int $id): T (throws NotFoundException) // - findBy(array $criteria, ...): list<T> // - findOneBy(array $criteria): ?T // - persist(T $entity): void // - remove(T $entity): void // - delete(int $id): void // - flush(): void
βοΈ Configuration (.mlc)
All framework config uses the .mlc format with environment variable interpolation:
# config/database.mlc
database {
driver = mysql
host = ${DB_HOST:127.0.0.1}
port = ${DB_PORT:3306}
name = ${DB_NAME:monkeyslegion}
user = ${DB_USER:root}
pass = ${DB_PASS:}
options {
charset = utf8mb4
collation = utf8mb4_unicode_ci
strict = true
}
pool {
min = 2
max = 10
idle = 300
}
}
# config/cache.mlc
cache {
default = redis
stores {
file {
driver = file
path = ${CACHE_PATH:storage/cache}
ttl = 3600
}
redis {
driver = redis
host = ${REDIS_HOST:127.0.0.1}
port = ${REDIS_PORT:6379}
prefix = ml_cache_
ttl = 3600
}
}
}
# config/auth.mlc
auth {
default_guard = jwt
guards {
jwt {
driver = jwt
secret = ${JWT_SECRET}
access_ttl = ${JWT_ACCESS_TTL:1800}
refresh_ttl = ${JWT_REFRESH_TTL:604800}
algorithm = HS256
issuer = ${APP_URL:http://localhost}
}
session {
driver = session
user_provider = database
}
}
}
The only PHP config file is config/app.php β reserved exclusively for DI container bindings:
return [ LoggerInterface::class => fn() => new Logger('app'), EventDispatcherInterface::class => fn($c) => $c->get(EventDispatcher::class), ];
π¦ Package Ecosystem (Detailed)
MonkeysLegion is built as a modular ecosystem of packages. Below is comprehensive documentation for each.
π§ Core Framework
monkeyslegion (Meta-package)
Installs the complete MonkeysLegion stack:
composer require monkeyscloud/monkeyslegion
monkeyslegion-core
Core runtime: kernel, Application builder, service provider scanner, bootstrapping.
monkeyslegion-di
PSR-11 dependency injection with attributes:
use MonkeysLegion\DI\Attributes\Singleton; use MonkeysLegion\DI\Attributes\ServiceProvider; #[Singleton] final class PaymentGateway { /* auto-registered as singleton */ } #[ServiceProvider] final class AppProvider { public function register(): void { // Bind interfaces to implementations } }
monkeyslegion-mlc
Production-ready .mlc configuration file parser:
- π Secure β Path traversal prevention, file permission checks
- β‘ Fast β File-based caching with automatic invalidation
- π― Type-Safe β Strong typing with
getString(),getInt(),getBool(),getArray()
use MonkeysLegion\Mlc\Loader; use MonkeysLegion\Mlc\Parser; $loader = new Loader(new Parser(), config_path()); $config = $loader->load(['app', 'database', 'cache']); $port = $config->getInt('database.port', 3306); $debug = $config->getBool('app.debug', false); $hosts = $config->getArray('database.hosts', []); $secret = $config->getRequired('app.secret'); // throws if missing
π HTTP & Routing
monkeyslegion-http
PSR-7 HTTP message implementations with factory methods:
use MonkeysLegion\Http\Message\Response; // Response factories (v2) Response::json(['data' => $users]); Response::json(['error' => 'Not found'], 404); Response::html($renderedHtml); Response::noContent(); // 204 Response::redirect('/dashboard', 302);
monkeyslegion-router
Attribute-based HTTP router with middleware, named routes, constraints, and caching.
Attribute-Based Controllers (v2):
use MonkeysLegion\Router\Attributes\Route; use MonkeysLegion\Router\Attributes\RoutePrefix; use MonkeysLegion\Router\Attributes\Middleware; use MonkeysLegion\Router\Attributes\Throttle; use MonkeysLegion\Auth\Attribute\Authenticated; use MonkeysLegion\Auth\Attribute\RequiresRole; use MonkeysLegion\Auth\Attribute\RequiresPermission; use MonkeysLegion\Auth\Attribute\Can; #[RoutePrefix('/api/v2/posts')] #[Middleware(['cors'])] final class PostController { // Public endpoint β no auth needed #[Route('GET', '/', name: 'posts.index', summary: 'List posts', tags: ['Posts'])] public function index(ServerRequestInterface $request): Response { $search = $request->getQueryParams()['q'] ?? null; $posts = $search !== null ? $this->posts->search($search) : $this->posts->findPublished(); return PostResource::collection($posts); } // Auth required #[Route('POST', '/', name: 'posts.create')] #[Authenticated] public function create(CreatePostRequest $dto, ServerRequestInterface $request): Response { $post = $this->service->createPost($dto, $request->getAttribute('user')); return PostResource::make($post, 201); } // Permission-based #[Route('POST', '/{id:\d+}/publish', name: 'posts.publish')] #[RequiresPermission('posts.publish')] public function publish(string $id): Response { $post = $this->service->publish((int) $id); return PostResource::make($post); } // Policy-based #[Route('DELETE', '/{id:\d+}', name: 'posts.destroy')] #[Can('delete', Post::class)] public function destroy(string $id): Response { $this->service->deletePost((int) $id); return Response::noContent(); } }
Route Constraints:
#[Route('GET', '/{id:\d+}')] // Digits only #[Route('GET', '/{slug:slug}')] // Slug format (a-z0-9-) #[Route('GET', '/{uuid:uuid}')] // UUID format #[Route('GET', '/{email:email}')] // Email format #[Route('GET', '/{amount:numeric}')] // Numeric values #[Route('GET', '/{name:alpha}')] // Alphabetic only #[Route('GET', '/{code:alphanum}')] // Alphanumeric #[Route('GET', '/{page?}')] // Optional parameter
Middleware Registration:
// config/middleware.mlc middleware { global = ["cors", "timing"] aliases { cors = "MonkeysLegion\\Router\\Middleware\\CorsMiddleware" throttle = "MonkeysLegion\\Router\\Middleware\\ThrottleMiddleware" auth = "MonkeysLegion\\Auth\\Middleware\\AuthenticationMiddleware" timing = "App\\Middleware\\TimingMiddleware" } groups { api = ["cors", "throttle:60,1", "auth"] web = ["cors", "csrf", "session"] } }
URL Generation:
$url = $router->url('users.show', ['id' => 123]); // Output: /api/v2/users/123 $url = $router->url('users.show', ['id' => 123], absolute: true); // Output: https://example.com/api/v2/users/123 // Extra params become query string $url = $router->url('posts.index', ['q' => 'php', 'page' => 2]); // Output: /api/v2/posts?q=php&page=2
Route Caching (Production):
use MonkeysLegion\Router\RouteCache; $cache = new RouteCache(__DIR__ . '/var/cache'); if ($cache->has()) { $collection->import($cache->load()); } else { // Register all routes... $cache->save($collection->export()['routes'], $collection->export()['namedRoutes']); } // Clear on deploy $cache->clear();
OpenAPI Metadata:
#[Route(
'GET', '/users',
name: 'users.index',
summary: 'List all users',
description: 'Returns a paginated list of users with optional filters',
tags: ['Users', 'API'],
meta: ['version' => '2.0', 'deprecated' => false],
)]
public function index(): Response { }
πΎ Database & ORM
monkeyslegion-database
Native PDO MySQL 8.4 connection manager. Configured via .mlc:
# config/database.mlc
database {
driver = mysql
host = ${DB_HOST:127.0.0.1}
port = ${DB_PORT:3306}
name = ${DB_NAME:monkeyslegion}
user = ${DB_USER:root}
pass = ${DB_PASS:}
}
monkeyslegion-query
Fluent Query Builder & Micro-ORM with EntityRepository.
Basic Queries:
use MonkeysLegion\Query\QueryBuilder; $qb = new QueryBuilder($connection); // Simple query $users = $qb->from('users') ->where('status', '=', 'active') ->orderBy('created_at', 'DESC') ->limit(10) ->fetchAll(); // With joins $posts = $qb->from('posts', 'p') ->leftJoin('users', 'u', 'u.id', '=', 'p.user_id') ->select(['p.*', 'u.name as author']) ->where('p.published', '=', true) ->fetchAll();
WHERE Clauses:
$qb->where('status', '=', 'active') ->where('age', '>', 18) ->orWhere('role', '=', 'admin'); // IN / BETWEEN / NULL $qb->whereIn('id', [1, 2, 3, 4, 5]) ->whereBetween('age', 18, 65) ->whereNull('deleted_at'); // Grouped conditions $qb->where('status', '=', 'active') ->whereGroup(function($q) { $q->where('role', '=', 'admin') ->orWhere('role', '=', 'moderator'); }); // β WHERE status = 'active' AND (role = 'admin' OR role = 'moderator')
Insert / Update / Delete:
$userId = $qb->insert('users', ['name' => 'Alice', 'email' => 'alice@example.com']); $qb->insertBatch('users', [ ['name' => 'Alice', 'email' => 'alice@example.com'], ['name' => 'Bob', 'email' => 'bob@example.com'], ]); $qb->update('users', ['status' => 'inactive']) ->where('last_login', '<', date('Y-m-d', strtotime('-1 year'))) ->execute(); $qb->delete('users')->where('status', '=', 'deleted')->execute();
Aggregates & Pagination:
$total = $qb->from('users')->count(); $revenue = $qb->from('orders')->sum('amount'); $avgPrice = $qb->from('products')->avg('price'); $result = $qb->from('posts') ->where('published', '=', true) ->paginate(page: 2, perPage: 15); // Returns: ['data' => [...], 'total' => 150, 'page' => 2, 'lastPage' => 10]
Transactions:
$result = $qb->transaction(function($qb) { $userId = $qb->insert('users', ['name' => 'Alice']); $qb->insert('profiles', ['user_id' => $userId]); return $userId; });
monkeyslegion-entity
Attribute-based data-mapper with v2 property hooks:
use MonkeysLegion\Entity\Attributes\Entity; use MonkeysLegion\Entity\Attributes\Field; use MonkeysLegion\Entity\Attributes\Id; use MonkeysLegion\Entity\Attributes\Timestamps; use MonkeysLegion\Entity\Attributes\SoftDeletes; use MonkeysLegion\Entity\Attributes\ManyToOne; #[Entity(table: 'posts')] #[Timestamps] #[SoftDeletes] final class Post { #[Id] #[Field(type: 'unsignedBigInt', autoIncrement: true)] public private(set) int $id; #[Field(type: 'string', length: 255)] public string $title; // Auto-slugification hook #[Field(type: 'string', length: 300)] public string $slug { set(string $value) { $s = preg_replace('/[^a-z0-9\s-]/', '', strtolower(trim($value))) ?? ''; $this->slug = trim(preg_replace('/[\s-]+/', '-', $s) ?? '', '-'); } } #[Field(type: 'text')] public string $body; // Computed excerpt β no DB column public string $excerpt { get => mb_strimwidth(strip_tags($this->body), 0, 160, 'β¦'); } public bool $isPublished { get => $this->published_at !== null; } #[ManyToOne(target: User::class)] public User $author; #[Field(type: 'datetime', nullable: true)] public ?\DateTimeImmutable $published_at = null; public function publish(): void { $this->published_at = new \DateTimeImmutable(); } public function unpublish(): void { $this->published_at = null; } }
monkeyslegion-migration
Entity-schema diff engine and SQL migration runner:
php ml make:migration # Generate migration from entity diff php ml migrate # Run pending migrations php ml rollback # Revert last migration php ml schema:update # Sync entities β database php ml schema:update --dump # Show SQL without executing
π Authentication & Security
monkeyslegion-auth
Comprehensive authentication and authorization:
- π JWT Authentication β Stateless auth with access/refresh token pairs
- π₯ RBAC β Role-based access control with permission inheritance + wildcards
- π Two-Factor (2FA) β TOTP compatible with Google Authenticator
- π OAuth β Google, GitHub providers (easily extensible)
- ποΈ API Keys β Scoped keys for M2M authentication
- β±οΈ Rate Limiting β Brute force protection
JWT Setup (v2 β via .mlc):
# config/auth.mlc
auth {
default_guard = jwt
guards {
jwt {
driver = jwt
secret = ${JWT_SECRET}
access_ttl = 1800 # 30 minutes
refresh_ttl = 604800 # 7 days
algorithm = HS256
}
}
}
Auth Controller (v2):
#[RoutePrefix('/api/v2/auth')] #[Middleware(['cors'])] final class AuthController { public function __construct( private readonly UserRepository $users, private readonly UserService $userService, ) {} #[Route('POST', '/login', name: 'auth.login')] #[Throttle(max: 5, per: 60)] public function login(LoginRequest $dto): Response { $user = $this->users->findByEmail($dto->email); if ($user === null || !password_verify($dto->password, $user->password_hash)) { return Response::json(['error' => 'Invalid credentials'], 401); } return Response::json([ 'data' => [ 'message' => 'Login successful', 'user_id' => $user->id, // In production: generate JWT via AuthService ], ]); } #[Route('POST', '/register', name: 'auth.register')] #[Throttle(max: 3, per: 60)] public function register(CreateUserRequest $dto): Response { $existing = $this->users->findByEmail($dto->email); if ($existing !== null) { return Response::json([ 'error' => 'Validation failed', 'details' => ['email' => 'Email already registered'], ], 422); } $user = $this->userService->createUser($dto); return Response::json([ 'data' => ['message' => 'Registration successful', 'email' => $user->email], ], 201); } #[Route('POST', '/logout', name: 'auth.logout')] #[Authenticated] public function logout(ServerRequestInterface $request): Response { return Response::json(['data' => ['message' => 'Logged out successfully']]); } }
Authorization Attributes (v2):
#[Authenticated] // Must be logged in #[RequiresRole('admin')] // Must have 'admin' role #[RequiresRole('admin', 'moderator')] // Must have ANY listed role #[RequiresPermission('posts.create')] // Must have specific permission #[Can('update', Post::class)] // Policy-based (PostPolicy::update) #[Throttle(max: 60, per: 1)] // Rate limiting
RBAC with Wildcards:
// Roles and permissions are stored on the User entity // Permission matching supports wildcards: $user->hasPermission('posts.view'); // exact match $user->hasPermission('posts.*'); // wildcard: posts.view, posts.create, etc. $user->hasPermission('*'); // super-admin: matches everything
2FA Setup:
use MonkeysLegion\Auth\TwoFactor\TotpProvider; use MonkeysLegion\Auth\Service\TwoFactorService; $twoFactor = new TwoFactorService(new TotpProvider(), issuer: 'YourApp'); // Generate setup data (QR code) $setup = $twoFactor->generateSetup($user->email); // Returns: secret, qr_code (base64), uri, recovery_codes // Verify and enable $twoFactor->enable($setup['secret'], $code, $user->id);
π Caching & Storage
monkeyslegion-cache
PSR-16 compliant cache with multiple drivers (File, Redis, Memcached, Array).
Configuration (v2 .mlc):
cache {
default = redis
stores {
file { driver = file, path = storage/cache }
redis { driver = redis, host = ${REDIS_HOST:127.0.0.1}, port = 6379 }
}
}
Usage:
use MonkeysLegion\Cache\Cache; Cache::set('key', 'value', 3600); $value = Cache::get('key', 'default'); Cache::delete('key'); // Remember pattern $users = Cache::remember('users', 3600, function() { return $this->users->findAll(); }); // Tagging Cache::tags(['users', 'premium'])->set('user:1', $user, 3600); Cache::tags(['users'])->clear(); // Incrementing Cache::increment('counter'); Cache::decrement('counter', 5);
monkeyslegion-files
Production-ready file storage and upload management:
- π Chunked Uploads β Resume-capable multipart uploads
- βοΈ Multi-Storage β Local, S3, MinIO, DigitalOcean, GCS
- πΌοΈ Image Processing β Thumbnails, optimization, watermarks
- π Security β Signed URLs, rate limiting
use MonkeysLegion\Files\FilesManager; $path = $files->put($_FILES['upload']['tmp_name']); $contents = $files->get($path); $url = ml_files_sign_url('/files/' . $path, ttl: 600);
Image Processing:
use MonkeysLegion\Files\Image\ImageProcessor; $processor = new ImageProcessor(driver: 'gd', quality: 85); $thumbPath = $processor->thumbnail($path, 300, 300, 'cover'); $optimized = $processor->optimize($path, quality: 80); $webp = $processor->convert($path, 'webp'); $watermarked = $processor->watermark($path, $watermarkPath, 'bottom-right');
Chunked Uploads:
use MonkeysLegion\Files\Upload\ChunkedUploadManager; $uploadId = $chunked->initiate('large-video.mp4', $totalSize, 'video/mp4'); foreach ($chunks as $i => $chunk) { $chunked->uploadChunk($uploadId, $i, $chunk['data'], $chunk['size']); } $finalPath = $chunked->complete($uploadId); $progress = $chunked->getProgress($uploadId); // ['uploaded_chunks' => 5, 'total_chunks' => 10, 'percent' => 50]
π¨ Templating & Views
monkeyslegion-template
MLView template engine with components, slots, and caching:
// resources/views/welcome.ml.php {{-- Escaped output --}} <h1>{{ $title }}</h1> {{-- Raw HTML --}} {!! $html !!} {{-- Control structures --}} @if ($user->isAdmin()) <span class="badge">Admin</span> @endif @foreach ($items as $item) <li>{{ $item->name }}</li> @endforeach {{-- Components --}} <x-alert type="success"> Operation completed! </x-alert> {{-- Layout inheritance --}} @extends('layouts.app') @section('content') <p>Page content here</p> @endsection {{-- Slots --}} <x-card> @slot('header') Card Title @endslot Card body content </x-card>
π§ Communication & Events
monkeyslegion-mail
Feature-rich mail package with DKIM, queues, and templates:
Configuration (v2 .mlc):
# config/mail.mlc
mail {
default = smtp
from {
address = ${MAIL_FROM_ADDRESS:noreply@example.com}
name = ${MAIL_FROM_NAME:MonkeysLegion}
}
smtp {
host = ${MAIL_HOST:smtp.mailtrap.io}
port = ${MAIL_PORT:587}
encryption = ${MAIL_ENCRYPTION:tls}
username = ${MAIL_USERNAME:}
password = ${MAIL_PASSWORD:}
}
}
Sending (v2):
use MonkeysLegion\Mail\Mailer; $mailer->send( 'user@example.com', 'Welcome to Our App', '<h1>Welcome!</h1><p>Thanks for joining us.</p>', 'text/html' );
Mailable Classes:
use MonkeysLegion\Mail\Mail\Mailable; class OrderConfirmationMail extends Mailable { public function __construct( private array $order, private array $customer, ) { parent::__construct(); } public function build(): self { return $this->view('emails.order-confirmation') ->subject('Order Confirmation #' . $this->order['id']) ->withData(['order' => $this->order, 'customer' => $this->customer]) ->attach('/path/to/invoice.pdf'); } } // Send or queue $mail = new OrderConfirmationMail($order, $customer); $mail->setTo('john@example.com')->send(); $mail->setTo('john@example.com')->queue();
monkeyslegion-events
PSR-14 event dispatcher with attribute-based listener discovery:
// Register listeners via attributes β no manual wiring needed #[Listener(UserCreated::class)] final class SendWelcomeEmail { /* ... */ } #[Listener(PostPublished::class)] final class NotifyAdminOnPost { /* ... */ } // Dispatch events $this->events->dispatch(new UserCreated($user)); $this->events->dispatch(new PostPublished($post));
π Internationalization
monkeyslegion-i18n
Production-ready I18n & localization:
- π Multiple Sources β JSON, PHP, database loaders
- π ICU Pluralization β Plural rules for 200+ languages
- π― Auto Detection β URL, session, headers, cookies
Translation Files:
// resources/lang/en/messages.json { "welcome": "Welcome!", "greeting": "Hello, :name!", "items": "{0} No items|{1} One item|[2,*] :count items" }
Usage:
use MonkeysLegion\I18n\TranslatorFactory; $translator = TranslatorFactory::create([ 'locale' => 'es', 'fallback' => 'en', 'path' => base_path('resources/lang'), ]); echo $translator->trans('messages.welcome'); // Output: Β‘Bienvenido! echo $translator->trans('messages.greeting', ['name' => 'Jorge']); // Output: Β‘Hola, Jorge! echo $translator->choice('messages.items', 5); // Output: 5 artΓculos
Helper Functions:
trans('messages.welcome'); trans('messages.greeting', ['name' => 'Jorge']); trans_choice('cart.items', $count);
π Observability & Logging
monkeyslegion-telemetry
Prometheus metrics, distributed tracing, and structured logging:
Configuration (v2 .mlc):
# config/logging.mlc
logging {
default = stack
channels {
stack {
driver = stack
channels = ["daily", "stderr"]
}
daily {
driver = daily
path = ${LOG_PATH:storage/logs/app.log}
days = 14
level = ${LOG_LEVEL:debug}
}
stderr {
driver = stream
stream = php://stderr
level = error
}
}
}
Metrics:
use MonkeysLegion\Telemetry\Telemetry; Telemetry::counter('http_requests_total', 1, ['method' => 'GET', 'status' => '200']); Telemetry::gauge('active_connections', 42); Telemetry::histogram('request_duration_seconds', 0.123, ['endpoint' => '/api/users']); $stopTimer = Telemetry::timer('operation_duration_seconds'); $this->heavyOperation(); $duration = $stopTimer(['operation' => 'heavy_task']);
Distributed Tracing:
$result = Telemetry::trace('fetch-user', function () use ($userId) { return $this->users->find($userId); }, SpanKind::CLIENT, ['user.id' => $userId]); // Nested traces (automatic parent-child) $result = Telemetry::trace('process-order', function () use ($order) { $inventory = Telemetry::trace('check-inventory', fn() => $this->inventory->check($order)); $payment = Telemetry::trace('process-payment', fn() => $this->payment->charge($order)); return compact('inventory', 'payment'); }); $traceId = Telemetry::traceId();
π CLI & Development
monkeyslegion-cli
Command-line interface and scaffolding:
# General php ml key:generate # Generate APP_KEY php ml cache:clear # Clear caches php ml route:list # Display routes with methods, middleware php ml tinker # Interactive REPL # Database php ml db:create # Create database php ml make:migration # Generate migration php ml migrate # Run pending migrations php ml rollback # Undo last migration php ml db:seed # Run seeders # Scaffolding php ml make:entity User # Generate entity with property hooks php ml make:controller User # Generate controller with #[Route] attributes php ml make:middleware Auth # Generate PSR-15 middleware php ml make:policy User # Generate authorization policy # API php ml openapi:export # Export OpenAPI 3.1 spec # Mail php ml mail:test user@test.com # Test sending php ml make:mail WelcomeMail # Generate Mailable class php ml make:dkim-pkey storage/keys # Generate DKIM keys php ml mail:work # Process mail queue # Cache php ml cache:clear # Clear default store php ml cache:clear --store=redis # Clear specific store
monkeyslegion-dev-server
Hot-reload development server:
composer serve # Start on localhost:8000 composer server:start:public # Start on 0.0.0.0:8000 composer server:stop # Stop server composer server:restart # Restart server
π§ Helper Functions
// Path helpers base_path('config/app.mlc'); // β /var/www/my-app/config/app.mlc app_path('Entity'); // β /var/www/my-app/app/Entity config_path('auth.mlc'); // β /var/www/my-app/config/auth.mlc storage_path('logs/app.log'); // β /var/www/my-app/storage/logs/app.log // Asset helpers (versioned URLs) asset('css/app.css'); // β /assets/css/app.css?v=1713312000 // Translation helpers trans('messages.welcome'); trans('messages.greeting', ['name' => 'Jorge']); // CSRF helpers csrf_token(); // β random 64-char hex string csrf_field(); // β <input type="hidden" name="_csrf" .../> // Auth helpers auth_user_id(); // β int|null auth_check(); // β bool
π§ͺ Testing
Test Suite (139 tests, 289 assertions)
# Run all unit tests composer test # Run specific suites php vendor/bin/phpunit --testsuite=Unit php vendor/bin/phpunit --testsuite=Integration php vendor/bin/phpunit --testsuite=Feature php vendor/bin/phpunit --testsuite=Performance # Run with coverage (requires PCOV/Xdebug) php vendor/bin/phpunit --coverage-text # Run benchmarks php tests/Performance/benchmark_detailed.php
Test Structure
| Suite | Tests | Coverage |
|---|---|---|
| Entity (User, Post, Role, Comment, RBAC) | 30 | Property hooks, computed props, validation, relationships |
| Enum (UserRole, OrderStatus) | 11 | Backed values, business logic, isFinal(), color() |
| DTO (all 4 request types) | 16 | Construction, readonly, nullable, validation attributes |
| Service (User, Post, Auth) | 10 | Create, find, delete, auth attempt, token invalidation |
| Controller (Home, Page, Auth, User, Post) | 17 | All endpoints, 401/404/422 error handling |
| Resource (User, Post) | 9 | toArray, make, collection, empty collection |
| Event/Listener | 9 | Construction, timestamps, dispatch, #[Listener] attributes |
| Policy (PostPolicy) | 7 | Author/admin/editor authorization scenarios |
| Job (SendWelcomeEmail) | 4 | Handle found/not-found, failed(), ShouldQueue |
| Middleware (Timing) | 2 | Server-Timing header injection, passthrough |
| Provider (AppProvider) | 3 | Register, #[Provider] attribute, instantiation |
| Helpers | 4 | base_path(), app_path(), CSRF token/field |
| Performance | 11 | Entity creation, hooks, serialization benchmarks |
Test Harness
// IntegrationTestCase β DI bootstrapping + PSR-15 pipeline use Tests\Integration\IntegrationTestCase; final class UserApiTest extends IntegrationTestCase { public function testListUsersReturns200(): void { $request = $this->createRequest('GET', '/api/v2/users'); $response = $this->dispatch($request); $this->assertStatus($response, 200); $this->assertJsonResponse($response, ['data' => [...]]); } } // FeatureTestCase β Full HTTP pipeline via Application::create()->boot() use Tests\Feature\FeatureTestCase; final class HomePageTest extends FeatureTestCase { public function testHomePageReturns200(): void { $request = $this->createRequest('GET', '/'); $response = $this->dispatch($request); $this->assertStatus($response, 200); $this->assertStringContainsString('MonkeysLegion', (string) $response->getBody()); } }
π Performance
Benchmarks (PHP 8.5, Apple Silicon)
| Operation | Ops/sec | vs Laravel 12 | vs Symfony 7 |
|---|---|---|---|
| Entity creation | 6.3M | ~140x | ~114x |
| DTO construction | 10.9M | ~60x | ~54x |
| Property hooks (email normalize) | 11.1M | N/A (PHP 8.4 exclusive) | |
| Computed properties (displayName) | 41M | N/A (PHP 8.4 exclusive) | |
| Enum operations (label+color+isFinal) | 8.7M | ~25x | ~22x |
| Resource serialization (50-item) | 43.8K | ~5.5x | ~3.6x |
| JSON encode+decode (50-item) | 21.5K | β | β |
| Peak memory | 4 MB | ~22 MB | ~14 MB |
HTTP Throughput (estimated)
| Framework | req/sec |
|---|---|
| MonkeysLegion v2 | ~12,500 |
| Slim 4 + PSR-15 | ~8,200 |
| Symfony 7.2 | ~4,800 |
| Laravel 12 | ~2,100 |
| CakePHP 5 | ~1,800 |
# Run full benchmark suite
php tests/Performance/benchmark_detailed.php
π Requirements
- PHP 8.4+ β Required for property hooks and asymmetric visibility
- MySQL 8.4 β Recommended database
- Composer 2.x β Dependency management
Recommended PHP Extensions
| Extension | Purpose |
|---|---|
pdo_mysql |
Database connectivity |
redis |
Caching, rate limiting, session storage |
mbstring |
Multi-byte string handling |
json |
JSON processing |
gd or imagick |
Image processing |
intl |
Advanced I18n formatting |
posix |
CLI process management |
pcntl |
Signal handling |
π Code Standards
- PHP 8.4+ with
declare(strict_types=1)on every file - 4-space indentation, LF line endings, UTF-8
finalclasses by default- Property hooks for validation/formatting β no getters/setters
public private(set)for auto-incremented IDsfinal readonlyfor events, DTOs- PSR-14 for events, PSR-15 for middleware, PSR-7 for messages
- PHPStan Level 9 enforced
- PHPUnit 11 with attributes (
#[Test],#[CoversClass],#[DataProvider])
See monkeyslegion_v2_code_standards.md for the complete standards document.
π€ Contributing
- Fork π΄
- Create a feature branch π±
- Submit a PR π
Happy hacking with MonkeysLegion! π
π License
MIT License β see LICENSE for details.
Contributors
![]() Jorge Peraza |
![]() Amanar Marouane |

