codemonster-ru / annabel
Elegant and lightweight PHP framework for modern web applications
Requires
- php: >=8.2
- codemonster-ru/api-resource: ^1.0
- codemonster-ru/auth: ^1.0
- codemonster-ru/cache: ^1.0
- codemonster-ru/config: ^2.1
- codemonster-ru/database: ^2.1
- codemonster-ru/env: ^2.0
- codemonster-ru/errors: ^1.2
- codemonster-ru/events: ^1.0
- codemonster-ru/filesystem: ^1.0
- codemonster-ru/http: ^2.1
- codemonster-ru/http-client: ^1.0
- codemonster-ru/logging: ^1.0
- codemonster-ru/mail: ^1.0
- codemonster-ru/queue: ^1.0
- codemonster-ru/razor: ^1.1
- codemonster-ru/router: ^2.6
- codemonster-ru/scheduler: ^1.0
- codemonster-ru/security: ^1.1
- codemonster-ru/session: ^2.0.1
- codemonster-ru/support: ^1.4
- codemonster-ru/validation: ^1.0
- codemonster-ru/view: ^2.0
- codemonster-ru/view-php: ^2.1
- psr/container: ^2.0
- psr/event-dispatcher: ^1.0
- psr/http-message: ^2.0
- psr/http-server-middleware: ^1.0
- psr/log: ^3.0
- psr/simple-cache: ^3.0
Requires (Dev)
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^9.6 || ^10.5 || ^11.0 || ^12.0
Suggests
- codemonster-ru/security: Enable CSRF protection and rate limiting integration
- codemonster-ru/ssr-bridge: Enable Vue/React/Svelte SSR rendering bridge
README
Important
This repository is read-only.
Development happens in the Annabel monorepo: https://github.com/codemonster-ru/annabel
Issues and pull requests should be opened there.
Annabel
Elegant and lightweight PHP framework for modern web applications.
Installation
composer require codemonster-ru/annabel
Quick Start
// public/index.php require __DIR__ . '/../vendor/autoload.php'; $app = require __DIR__ . '/../bootstrap/app.php'; $app->run(); // bootstrap/app.php use Codemonster\Annabel\Application; $baseDir = __DIR__ . '/..'; $app = new Application($baseDir); require "$baseDir/routes/web.php"; return $app; // routes/web.php router()->get('/', fn() => view('home', ['title' => 'Welcome to Annabel']));
CLI
Annabel ships with a lightweight CLI similar to Laravel's artisan. It already supports:
about- show version, base path, and loaded providersroute:list- list registered routesconfig:get key- read a config valueconfig:list- list config values with secrets redactedcontainer:list- show container bindings/instancesvendor:publish- publish package config, migrations, views, or assetsserve- run PHP built-in server (default 127.0.0.1:8000)make:controller,make:model,make:middleware,make:request,make:policy- generate application classes- With
codemonster-ru/databaseinstalled:make:migration,migrate,migrate:rollback,migrate:status,make:seed,seed(appear inannabel list; connection is checked when commands run)
php vendor/bin/annabel php vendor/bin/annabel help php vendor/bin/annabel help list php vendor/bin/annabel make:controller Admin/User php vendor/bin/annabel make:model User php vendor/bin/annabel make:policy Post php vendor/bin/annabel queue:work --once php vendor/bin/annabel schedule:run
Commands may be registered by service providers and are resolved through the application container, including constructor dependency injection:
class PackageServiceProvider extends ServiceProvider { public function register(): void { $this->commands([ SyncPackageCommand::class, ]); } }
New commands may implement execute(InputInterface $input, OutputInterface $output): int. ArgvInput provides positional arguments and parsed long
options; commands return ExitCode constants. The legacy handle(array) method
remains supported for existing commands.
Testing
Application tests can use Annabel's lightweight HTTP helpers:
use Codemonster\Annabel\Application; use Codemonster\Annabel\Testing\InteractsWithApplication; use PHPUnit\Framework\TestCase; class FeatureTest extends TestCase { use InteractsWithApplication; protected function createApplication(): Application { return require __DIR__ . '/../bootstrap/app.php'; } public function test_homepage(): void { $this->get('/')->assertOk()->assertSee('Welcome'); } }
Database Integration
Annabel ships with first-class integration for
codemonster-ru/database.
1. Create config/database.php
return [ 'default' => 'mysql', 'connections' => [ 'mysql' => [ 'driver' => 'mysql', 'host' => '127.0.0.1', 'port' => 3306, 'database' => env('DB_NAME'), 'username' => env('DB_USER'), 'password' => env('DB_PASS'), 'charset' => 'utf8mb4', ], 'sqlite' => [ 'driver' => 'sqlite', 'database' => base_path('database/database.sqlite'), ], ], ];
2. Usage
// Query builder $users = db()->table('users')->where('active', 1)->get(); // Schema builder schema()->create('posts', function ($table) { $table->id(); $table->string('title'); }); // Transactions transaction(function () { db()->table('logs')->insert(['type' => 'created']); });
Helpers
| Function | Description |
|---|---|
app() |
Access the application container |
base_path() |
Resolve base project paths |
config() |
Get or set configuration values |
env() |
Read environment variables |
dump() / dd() |
Debugging utilities |
request() |
Get current HTTP request |
response() / json() |
Create HTTP response |
http_client() |
Access the HTTP client |
router() |
Access router or register route |
route() |
Generate a named route URI |
view() |
Render or return view instance |
session() |
Access session store |
storage() |
Access filesystem storage disks |
old() |
Read flashed old form input |
errors() |
Read flashed validation errors |
auth() |
Access the authentication guard |
user() |
Read the authenticated user |
cache() |
Access PSR-16 cache store |
mailer() |
Access mailers |
queue() |
Access queue connections |
dispatch() |
Dispatch a queue job |
schedule() |
Access scheduled tasks |
validator() |
Validate input data |
db() |
Get the active database connection |
schema() |
Get the schema builder |
transaction() |
Execute a DB transaction |
All helpers are autoloaded automatically.
Filesystem
Annabel registers codemonster-ru/filesystem by default. Publish the default
config and use storage() to read or write files:
php vendor/bin/annabel vendor:publish --tag=filesystem
storage('public')->put('avatars/user-1.txt', 'avatar'); $url = storage('public')->url('avatars/user-1.txt');
HTTP Client
Annabel registers codemonster-ru/http-client by default. Configure defaults in
config/http-client.php and use http_client() for external API calls:
$response = http_client() ->baseUrl('https://api.example.com') ->acceptJson() ->get('/users/1'); $user = $response->throw()->json();
Middleware
Annabel supports PSR-15 middleware via Psr\Http\Server\MiddlewareInterface.
Route middleware may be registered by class name, and global middleware may be
added to the kernel with addMiddleware().
Middleware aliases and groups keep routes compact:
router()->get('/dashboard', [DashboardController::class, 'index']) ->middleware('auth'); router()->get('/posts/{post}', [PostController::class, 'show']) ->middleware('can:posts.view,post'); router()->post('/posts', [PostController::class, 'store']) ->middleware('web');
The framework registers auth and can when auth is enabled. The security
provider registers csrf, throttle, and the default web / api groups.
Custom aliases and groups can be registered on the HTTP kernel:
app(\Codemonster\Annabel\Http\Kernel::class) ->aliasMiddleware('admin', App\Http\Middleware\AdminOnly::class);
Publish the security config to tune CSRF, rate-limit storage, trusted proxies, and named throttle presets:
php vendor/bin/annabel vendor:publish --tag=security
router()->post('/login', [LoginController::class, 'store']) ->middleware('throttle:login');
Authentication
Annabel registers codemonster-ru/auth by default. Publish the default config
and configure a user provider in config/auth.php, or provide a small in-memory
list for local applications:
php vendor/bin/annabel vendor:publish --tag=auth
return [ 'provider' => 'database', 'database' => [ 'table' => 'users', 'identifier_column' => 'id', 'password_column' => 'password', ], 'users' => [ new App\User(1, 'admin@example.com', password_hash('secret', PASSWORD_DEFAULT)), ], 'redirect_to' => '/login', ];
if (auth()->attempt(['email' => $email, 'password' => $password])) { return response()->redirect('/dashboard'); } router()->get('/dashboard', [DashboardController::class, 'index']) ->middleware('auth');
Production applications should bind a database-backed
Codemonster\Auth\Contracts\UserProviderInterface implementation through
auth.provider.
Routing
Routes support dynamic parameters, constraints, names, and URI generation:
router()->get('/users/{id}', [UserController::class, 'show']) ->where('id', '\d+') ->name('users.show'); route('users.show', ['id' => 42]); // /users/42
Route parameters are injected into closures and controllers by parameter name.
The current Codemonster\Http\Request may be type-hinted alongside route
parameters.
API Resources
API resources provide one transformation for individual models, collections,
and existing simplePaginate() results:
use Codemonster\ApiResource\JsonResource; final class UserResource extends JsonResource { public function toArray(): array { return [ 'id' => $this->resource->getKey(), 'name' => $this->resource->name, ]; } } return UserResource::paginated( User::query()->simplePaginate(20, $page), '/api/users', )->response();
Logging
Annabel binds Psr\Log\LoggerInterface in the container. Configure the default
channel in config/logging.php; unhandled HTTP exceptions are reported before
the error response is rendered.
Cache
Annabel binds Psr\SimpleCache\CacheInterface in the container. Configure the
default store in config/cache.php; the framework ships with array, file,
and redis stores. Set CACHE_STORE=redis and configure REDIS_HOST,
REDIS_PORT, REDIS_PASSWORD, and REDIS_CACHE_DB for shared cache in
multi-instance deployments.
Annabel registers codemonster-ru/mail by default. Configure the default
mailer in config/mail.php; the framework ships with array, log,
sendmail, and Symfony-powered smtp transports. Set MAIL_MAILER=smtp and
provide an SMTP DSN through MAILER_DSN.
use Codemonster\Mail\Message; mailer('log')->send( Message::make() ->from('hello@example.com', 'Annabel') ->to('user@example.com') ->subject('Welcome') ->text('Welcome to Annabel.'), );
Queue
Annabel registers codemonster-ru/queue by default. Configure the default
connection in config/queue.php; the framework ships with sync, database,
and redis drivers.
use Codemonster\Queue\Contracts\JobInterface; class SendWelcomeEmailJob implements JobInterface { public function handle(): void { // } } dispatch(new SendWelcomeEmailJob());
The default sync connection runs jobs immediately. For SQL-backed background
jobs, set QUEUE_CONNECTION=database, publish queue migrations, run migrate,
and start the worker:
php vendor/bin/annabel vendor:publish --tag=queue-migrations php vendor/bin/annabel migrate php vendor/bin/annabel queue:work php vendor/bin/annabel queue:work --stop-when-empty php vendor/bin/annabel queue:failed php vendor/bin/annabel queue:retry 1 php vendor/bin/annabel queue:retry all php vendor/bin/annabel queue:flush
For Redis-backed workers, set QUEUE_CONNECTION=redis and configure
REDIS_HOST, REDIS_PORT, REDIS_PASSWORD, REDIS_QUEUE_DB, and
QUEUE_REDIS_PREFIX. Redis failed jobs are stored in Redis and work with the
same queue:failed, queue:retry, and queue:flush commands.
Scheduler
Annabel registers codemonster-ru/scheduler by default. Define tasks in
routes/schedule.php and run schedule:run every minute from cron:
use Codemonster\Scheduler\Schedule; /** @var Schedule $schedule */ $schedule->call(fn () => cleanup(), 'cleanup') ->dailyAt('03:00') ->withoutOverlapping();
* * * * * php /path/to/app/vendor/bin/annabel schedule:run
Use schedule:list to inspect registered tasks, cron expressions, due status,
and overlap locks.
Scheduler locks use the configured cache store when the cache provider is registered.
Production optimization
Build configuration and route caches during deployment:
php vendor/bin/annabel optimize
Routes with closures cannot be cached. Use controller handlers such as
[HomeController::class, 'index']. Clear all generated caches before changing
environment configuration or when troubleshooting:
php vendor/bin/annabel optimize:clear
The individual config:cache, config:clear, route:cache, and route:clear
commands are also available.
Events
Annabel binds Psr\EventDispatcher\EventDispatcherInterface and
Psr\EventDispatcher\ListenerProviderInterface. Register listeners through the
framework listener provider and dispatch events through the PSR dispatcher.
Validation
Annabel ships with a small validation layer for request/config data. It supports
common scalar rules, nested fields through dot notation, validated() data, and
validateOrFail() for exception-driven flows.
$result = validator([ 'email' => 'hello@example.com', ], [ 'email' => 'required|email', ]); if ($result->fails()) { $errors = $result->errors(); }
Controllers can use Codemonster\Annabel\Http\ValidatesRequests to validate the
current request. Validation failures return JSON 422 responses for API
requests, or redirect back with flashed errors and _old_input for web forms.
Redirects are restricted to local same-origin locations. Sensitive fields are
excluded recursively according to config/validation.php.
use Codemonster\Annabel\Http\ValidatesRequests; use Codemonster\Http\Request; class RegisterController { use ValidatesRequests; public function store(Request $request): mixed { $data = $this->validate($request, [ 'email' => 'required|email', ]); // ... } }
HTTP Exceptions
Framework HTTP exceptions live under Codemonster\Annabel\Http\Exceptions.
They expose stable status and header contracts for bad requests, authentication,
authorization, missing routes, and unsupported methods.
Container parameters
The Annabel container implements Psr\Container\ContainerInterface, so it can be
passed to libraries expecting a PSR-11 container.
You can pass named constructor parameters when resolving classes or closure bindings:
$user = app(User::class, ['name' => 'Annabel']); app()->bind(User::class, fn($container, array $params) => new User($params['name'])); $user = app(User::class, ['name' => 'Annabel']); // Same for Application::make() $user = $app->make(User::class, ['name' => 'Annabel']);
Note: for singleton bindings, passing parameters after the instance is resolved throws an exception.
Note: Application::serve() will throw if an instance already exists; call Application::resetInstance() first.
Providers
Annabel reads provider settings from config/app.php before registering services.
return [ 'providers' => [ 'defaults' => true, 'disabled' => [], 'extra' => [], 'discover' => true, 'path' => base_path('bootstrap/providers'), 'packages' => [ 'discover' => true, 'dont_discover' => [], 'cache' => true, 'cache_path' => base_path('bootstrap/cache/packages.php'), ], ], ];
All providers are registered first and booted after registration completes.
Installed packages may declare providers in Composer metadata:
{
"extra": {
"annabel": {
"providers": [
"Vendor\\Package\\PackageServiceProvider"
]
}
}
}
Only providers owned by that package should be declared. Applications can
disable selected packages through providers.packages.dont_discover, use *
to disable all package discovery, or set providers.packages.discover to
false. The generated manifest cache is invalidated when package
composer.json metadata changes.
Publishable Resources
Package providers may register publishable files or directory trees:
class PackageServiceProvider extends ServiceProvider { public function boot(): void { $this->publishes([ __DIR__ . '/../../config/package.php' => base_path('config/package.php'), __DIR__ . '/../../resources/views' => base_path('resources/views/vendor/package'), ], ['config', 'package']); } }
Publishing is explicit and does not overwrite existing files unless requested:
php vendor/bin/annabel vendor:publish --provider="Vendor\\Package\\PackageServiceProvider"
php vendor/bin/annabel vendor:publish --tag=config
php vendor/bin/annabel vendor:publish --all --force
Destinations must remain inside the application base path. Symbolic-link escape paths are rejected.
Testing
composer test