ObjectQuel ORM
ObjectQuel ORM provides an intuitive object-relational mapping layer with a purpose-built query language, automatic relationship management, and Data Mapper pattern architecture.
Installation
Install ObjectQuel in your Canvas project via Composer:
composer require quellabs/canvas-objectquel
Configure your database connection in config/database.php:
return [
'driver' => 'mysql',
'host' => 'localhost',
'database' => 'database',
'username' => 'root',
'password' => '',
'port' => 3306,
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
// Development mode
'development_mode' => true,
// Entity namespace
'entity_namespace' => 'App\\Entities',
// Path to the entities folder
'entity_path' => dirname(__FILE__) . '/../src/Entities/',
// Path to the proxy folder
'proxy_path' => dirname(__FILE__) . '/../storage/objectquel/proxies/',
// Path to the migrations folder
'migrations_path' => dirname(__FILE__) . '/../migrations',
];
Once installed and configured, ObjectQuel is automatically discovered by Canvas. The EntityManager becomes available via $this->em() in controllers that extend BaseController.
CLI Tools (Sculpt)
ObjectQuel includes Sculpt, a CLI tool for development tasks:
# Create new entity interactively
php vendor/bin/sculpt make:entity
# Generate entity from existing table
php vendor/bin/sculpt make:entity-from-table
# Generate and run migrations
php vendor/bin/sculpt make:migrations
php vendor/bin/sculpt quel:migrate
# Rollback migrations
php vendor/bin/sculpt quel:migrate --rollback
Entity Definition
Define entities using PHP annotations:
/**
* @Orm\Table(name="users")
*/
class UserEntity {
/**
* @Orm\Column(name="id", type="integer", length=11, primary_key=true)
* @Orm\PrimaryKeyStrategy(strategy="identity")
*/
private ?int $id = null;
/**
* @Orm\Column(name="name", type="string", length=255)
*/
private string $name;
/**
* @Orm\Column(name="email", type="string", length=255)
*/
private string $email;
/**
* @Orm\Column(name="created_at", type="datetime", nullable=true)
*/
private ?\DateTime $createdAt = null;
// Standard getters and setters
public function getId(): ?int { return $this->id; }
public function getName(): string { return $this->name; }
public function setName(string $name): void { $this->name = $name; }
// ...
}
Basic CRUD Operations
Standard find, persist, and remove operations:
// Find by ID
$user = $this->em()->find(UserEntity::class, $id);
// Find by criteria
$admins = $this->em()->findBy(UserEntity::class, [
'active' => true,
'role' => 'admin'
]);
// Create
$user = new UserEntity();
$user->setName('John Doe');
$user->setEmail('john@example.com');
$this->em()->persist($user);
$this->em()->flush();
// Update
$user->setEmail('newemail@example.com');
$this->em()->persist($user);
$this->em()->flush();
// Delete
$this->em()->remove($user);
$this->em()->flush();
ObjectQuel Query Language
Execute queries using ObjectQuel's declarative syntax:
// Basic query
$results = $this->em()->executeQuery("
range of p is App\\Entity\\PostEntity
retrieve (p) where p.published = :published
sort by p.createdAt desc
", [
'published' => true
]);
$posts = array_column($results, 'p');
// Query with relationships
$results = $this->em()->executeQuery("
range of u is App\\Entity\\UserEntity
range of p is App\\Entity\\PostEntity via u.posts
retrieve (u, p) where u.id = :userId
sort by p.createdAt desc
", [
'userId' => $authorId
]);
Entity Autowiring
Canvas automatically resolves entity parameters in controller methods. Type-hint an entity class and Canvas fetches it from the database using the route's primary key — no manual lookup required.
/**
* @Route("/posts/{id:int}")
* @param PostEntity|null $post
* @return Response
*/
public function show(?PostEntity $post): Response {
if (!$post) {
return $this->notFound('Post does not exist.');
}
return $this->render('blog/show.tpl', ['post' => $post]);
}
By convention, Canvas matches the {id} route parameter to the entity's primary key. The parameter is nullable — if no record is found, null is passed and you handle it in the controller.
Non-standard route parameters
When the route parameter is not named id, use the @ResolveEntity annotation to specify which route parameter holds the primary key. Pass the entity class using ::class syntax as the first argument:
/**
* @Route("/posts/{slug:string}")
* @ResolveEntity(PostEntity::class, routeParam="slug")
* @param PostEntity|null $post
* @return Response
*/
public function show(?PostEntity $post): Response {
if (!$post) {
return $this->notFound('Post does not exist.');
}
return $this->render('blog/show.tpl', ['post' => $post]);
}
Multiple entity parameters
When a method has more than one entity parameter, add a @ResolveEntity for each. The entity class identifies which parameter each annotation applies to:
/**
* @Route("/users/{userId:int}/posts/{postId:int}")
* @ResolveEntity(UserEntity::class, routeParam="userId")
* @ResolveEntity(PostEntity::class, routeParam="postId")
* @param UserEntity|null $user
* @param PostEntity|null $post
* @return Response
*/
public function show(?UserEntity $user, ?PostEntity $post): Response {
if (!$user || !$post) {
return $this->notFound();
}
return $this->render('blog/show.tpl', ['user' => $user, 'post' => $post]);
}
@ResolveEntity reference
| Parameter | Required | Description |
|---|---|---|
First argument |
When using multiple entity parameters or a non-standard route parameter | The entity class to resolve, using ClassName::class syntax. Refactor-safe — your IDE will update it automatically on rename. |
routeParam |
When route parameter is not named id |
The name of the route placeholder that contains the primary key value. Defaults to id. |
Learn More
For comprehensive documentation on ObjectQuel including advanced query syntax, caching strategies, transaction management, and performance optimization, visit the ObjectQuel documentation site.