Define routes with annotations. Separate concerns with aspects. Query with plain English. Build what matters.
Setup to First Route
Config Files Required
Open Source & Free
// No separate route files to maintain!
/**
* @Route("/api/users")
* @InterceptWith(AuthAspect::class)
*/
class UserController extends BaseController {
/**
* @Route("/")
* @InterceptWith(CacheAspect::class, ttl=300)
*/
public function index() {
// Pure business logic - aspects handle auth & caching
$users = $this->em->findBy(User::class, ['active' => true]);
return $this->json($users);
}
/**
* @Route("/{id}")
* @InterceptWith(ValidateAspect::class)
*/
public function show(int $id) {
// ObjectQuel makes queries readable
$user = $this->em->executeQuery("
range of u is UserEntity
retrieve (u) where u.id = :id
", ['id' => $id]);
return $this->json($user);
}
}
A unified framework, ORM, and curated toolset designed to work together.
Modern PHP framework with annotation-based routing, contextual containers, and aspect-oriented programming.
Annotation-Based Routing
Contextual Containers
Aspect-Oriented Programming
Zero Configuration
ObjectQuel ORM Integration
Powerful ORM with Data Mapper pattern, intuitive query language, and lifecycle events.
Data Mapper Architecture
ObjectQuel Query Language
Lifecycle Events & SignalHub
Entity Relationship Management
Migration & Schema Tools
Lightweight, flexible service discovery component with automatic provider detection and advanced caching.
Framework Agnostic Design
Multiple Discovery Methods
Provider Families & Organization
Advanced Caching System
PSR-4 Utilities
Type-safe signal-slot system for PHP with Qt-inspired design for loose coupling between components.
Type-Safe Signal-Slot System
Flexible Connection Patterns
Signal Discovery & Registry
Object-Owned & Standalone Signals
Priority-Based Execution
A modern, lightweight PHP framework that gets out of your way. Canvas combines annotation-based routing, contextual containers, ObjectQuel ORM, and aspect-oriented programming to create clean, maintainable code. Define routes directly in controllers with @Route annotations, query databases with intuitive ObjectQuel syntax, and add crosscutting concerns like authentication and caching without cluttering business logic.
Convention-over-configuration approach
Define routes directly in controllers
Contextual Containers: Intelligent service resolution based on context
Clean separation of crosscutting concerns
Automatic Discovery: Add functionality by requiring packages
namespace App\Controller;
use Quellabs\Canvas\Annotations\Route;
use Quellabs\Canvas\Annotations\InterceptWith;
use App\Aspects\RequireAuthAspect;
use App\Aspects\CacheAspect;
/**
* @InterceptWith(RequireAuthAspect::class)
*/
class UserController extends BaseController {
/**
* @Route("/users")
* @InterceptWith(CacheAspect::class, ttl=300)
*/
public function index() {
$users = $this->em->findBy(User::class,
['active' => true]
);
return $this->render('users/index.tpl', compact('users'));
}
}
ObjectQuel is a powerful Object-Relational Mapping system built on the Data Mapper pattern, offering clean separation between entities and persistence logic. It features a purpose-built query language inspired by QUEL that feels natural to developers, combined with structured data enrichment. It's powered by CakePHP's robust database foundation.
Clean separation between entities and persistence logic
Intuitive, object-oriented query syntax with pattern matching
Comprehensive event system for entity lifecycle management
Five relationship types with advanced junction table support
Automated entity generation and database migration tools
Built on CakePHP's proven database layer for reliability
// Natural, readable queries
$results = $em->executeQuery("
range of p is ProductEntity
range of c is CategoryEntity via p.categories
range of r is ReviewEntity via p.reviews
retrieve (p, c.name, r.rating)
where p.price < :maxPrice
and c.active = true
and r.rating >= 4
sort by r.rating desc
", ['maxPrice' => 50.00]);
// Pattern matching
$results = $em->executeQuery("
range of u is UserEntity
retrieve (u) where u.email = '*@company.com'
");
// Regular expressions
$results = $em->executeQuery("
range of p is ProductEntity
retrieve (p) where p.sku = /^[A-Z]{2}\d{4}$/
");
Quellabs Discover automatically discovers service providers across your application and its dependencies with advanced caching and lazy loading capabilities. It focuses solely on locating service providers, giving you complete control over how to use them in your application architecture. Unlike other solutions that force specific patterns, Discover is framework-agnostic and integrates into any PHP application.
Works with any PHP application or framework without dependencies
Composer configuration, directory scanning, and custom scanners
Organize providers into logical groups with family-based filtering
Lightning-fast performance with export/import caching capabilities
Built-in tools for namespace discovery and class finding
Efficient discovery using static methods without instantiation
// Automatic service discovery
$discover = new Discover();
$discover->addScanner(new ComposerScanner());
$discover->addScanner(new DirectoryScanner([
__DIR__ . '/app/Providers'
]));
// Discover services
$discover->discover();
// Get providers by family
$cacheProviders = $discover
->findProvidersByFamily('cache');
$databaseProviders = $discover
->findProvidersByFamily('database');
// Lightning-fast caching
$cacheData = $discover->exportForCache();
file_put_contents('cache/providers.json',
json_encode($cacheData)
);
SignalHub brings Qt-style signal-slot programming to PHP with strong type checking and flexible connection patterns. It enables loose coupling between components while maintaining type safety through runtime validation. Features both standalone signals and object-owned signals with powerful discovery capabilities through a centralized hub system.
Framework-agnostic design works with any PHP application architecture
Runtime type checking ensures all signal-slot connections are type compatible
Support for direct connections and wildcard pattern matching for flexible event handling
Centralized SignalHub for registering, finding, and managing signals across your application
Control execution order of connected slots with priority-based handler execution
use Quellabs\SignalHub\SignalHub;
use Quellabs\SignalHub\SignalHubLocator;
// Fetch the signal hub for registration and discovery
$hub = SignalHubLocator::getInstance();
// Create a standalone signal with a string parameter
$buttonClickedSignal = $hub->createSignal('button.clicked', ['string']);
// Connect a handler to the signal
$buttonClickedSignal->connect(function(string $buttonId) {
echo "Button clicked: {$buttonId}\n";
});
// Emit the signal
$buttonClickedSignal->emit('submit-button');
See how Canvas simplifies modern PHP development with annotation-based routing and aspect-oriented programming.
// routes/web.php
Route::middleware(['auth', 'admin'])->group(function () {
Route::get('/admin/users', [AdminController::class, 'users'])
->middleware('cache:300');
Route::get('/admin/reports', [AdminController::class, 'reports'])
->middleware('cache:3600');
});
// app/Http/Controllers/AdminController.php
class AdminController extends Controller {
public function __construct() {
$this->middleware('auth');
$this->middleware('admin');
}
public function users(Request $request) {
// Manual caching logic
$users = Cache::remember('admin.users', 300, function () {
return User::where('active', true)->get();
});
return view('admin.users', compact('users'));
}
}
// Everything in one place - no separate route files!
/**
* @InterceptWith(RequireAuthAspect::class)
* @InterceptWith(RequireAdminAspect::class)
*/
class AdminController extends BaseController {
/**
* @Route("/admin/users")
* @InterceptWith(CacheAspect::class, ttl=300)
*/
public function users() {
// Pure business logic - aspects handle everything else
$users = $this->em->findBy(User::class, ['active' => true]);
return $this->render('admin/users.tpl', compact('users'));
}
/**
* @Route("/admin/reports")
* @InterceptWith(CacheAspect::class, ttl=3600)
*/
public function reports() {
// Inherits auth + admin, adds longer cache
return $this->render('admin/reports.tpl');
}
}
// Multiple files to maintain
// app/Http/Middleware/AdminMiddleware.php
class AdminMiddleware {
public function handle($request, Closure $next) {
if (!auth()->user()->isAdmin()) {
return redirect('/login');
}
return $next($request);
}
}
// app/Http/Kernel.php
protected $routeMiddleware = [
'admin' => \App\Http\Middleware\AdminMiddleware::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
];
// Controller method
public function sensitiveAction(Request $request) {
// Business logic mixed with concerns
Log::info('Admin accessed sensitive data', [
'user_id' => auth()->id()
]);
return $this->processData();
}
// Single reusable aspect
class RequireAdminAspect implements BeforeAspect {
public function before(MethodContext $context): ?Response {
if (!$this->auth->getCurrentUser()?->isAdmin()) {
return new RedirectResponse('/login');
}
return null;
}
}
// Clean controller with declarative aspects
/**
* @InterceptWith(RequireAdminAspect::class)
* @InterceptWith(AuditLogAspect::class)
*/
class AdminController extends BaseController {
/**
* @Route("/admin/sensitive")
* @InterceptWith(RateLimitAspect::class, limit=5)
*/
public function sensitiveAction() {
// Pure business logic - aspects handle everything
return $this->processData();
}
}
// Eloquent ORM - Active Record pattern
$posts = Post::with(['author', 'comments' => function ($query) {
$query->where('approved', true)
->orderBy('created_at', 'desc');
}])
->where('published', true)
->where('created_at', '>=', now()->subDays(30))
->whereHas('author', function ($query) {
$query->where('active', true);
})
->orderBy('views', 'desc')
->take(10)
->get();
// Multiple database queries behind the scenes
// N+1 problems without careful eager loading
// ObjectQuel - Data Mapper with natural syntax
$posts = $this->em->executeQuery("
range of p is Post
range of a is Author via p.id
range of c is Comment via p.id
retrieve (p, a, c)
where p.published = true and
p.created_at >= :monthAgo and
a.active = true and
c.approved = true
sort by p.views desc
window 0 using window_size 10
", [
'monthAgo' => new DateTime('-30 days')
]);
// Single optimized query
// No N+1 problems - joins controlled by @RequiredRelation annotations
Stop fighting your framework. Start building features.
Define routes directly in controllers with annotations. No separate route files to maintain or hunt through.
Aspect-oriented programming handles auth, caching, loggingβyour controllers focus on what they should: business logic.
ObjectQuel uses natural language syntax instead of complex ORM chains. Write queries that actually make sense.
Ready to simplify your PHP development?
Zero configuration required. Start building immediately.