quellabs / objectquel
A sophisticated ORM system with a unique query language and streamlined architecture
Requires
- php: >=8.3
- ext-curl: *
- ext-fileinfo: *
- ext-gd: *
- ext-json: *
- ext-mysqli: *
- ext-pdo: *
- cakephp/database: ^4.4
- quellabs/annotation-reader: dev-main
- quellabs/contracts: dev-main
- quellabs/discover: dev-main
- quellabs/sculpt: dev-main
- quellabs/signal-hub: dev-main
- robmorgan/phinx: ^0.13.4
- softcreatr/jsonpath: *
This package is auto-updated.
Last update: 2025-05-23 11:40:20 UTC
README
ObjectQuel is a powerful Object-Relational Mapping (ORM) system built on the Data Mapper pattern, offering a clean separation between entities and persistence logic. It combines a purpose-built query language with structured data enrichment and is powered by CakePHP's robust database foundation under the hood.
Table of Contents
- The ObjectQuel Advantage
- Installation
- Quick Start
- Core Components
- Configuration
- Working with Entities
- The ObjectQuel Language
- Entity Relationships
- Indexing
- Saving and Persisting Data
- Using Repositories
- SignalHub
- Utility Tools
- Query Optimization
- License
The ObjectQuel Advantage
ObjectQuel addresses fundamental design challenges in object-relational mapping through its architecture:
- Entity-Based Query Language: The ObjectQuel language provides an intuitive, object-oriented syntax for database operations that feels natural to developers
- Data Mapper Architecture – Entities remain decoupled from the database, ensuring clean, testable domain logic.
- Powered by CakePHP's Database Layer – Reliable and battle-tested SQL engine under the hood.
- Relationship Simplicity: Work with complex relationships without complex query code
- Performance By Design: Multiple built-in optimization strategies for efficient database interactions
- Hybrid Data Sources: Uniquely combine traditional databases with external JSON data sources
Installation
Installation can be done through composer.
composer require quellabs/objectquel
Quick Start
This shows a quick way to use ObjectQuel:
// 1. Create configuration $config = new Configuration(); $config->setDsn('mysql://db_user:db_password@localhost:3306/my_database'); $config->setEntityNamespace('App\\Entity'); $config->setEntityPath(__DIR__ . '/src/Entity'); // 2. Create EntityManager $entityManager = new EntityManager($config); // 3. Find an entity $product = $entityManager->find(\App\Entity\ProductEntity::class, 101); // 4. Update and save $product->setPrice(29.99); $entityManager->persist($product); $entityManager->flush(); // 5. Query using ObjectQuel language $results = $entityManager->executeQuery(" range of p is App\\Entity\\ProductEntity range of c is App\\Entity\\CategoryEntity via p.categories retrieve (p) where p.price < :maxPrice ", [ 'maxPrice' => 50.00 ]);
Core Components
ObjectQuel consists of several primary components working together:
- EntityManager: Central wrapper around the various helper classes
- EntityStore: Manages entity classes and their relationships
- UnitOfWork: Tracks individual entities and their changes
- ObjectQuel: Handles reading and parsing of the ObjectQuel query language
- Query Decomposer: Analyzes a parsed query and methodically breaks it down into smaller, logically sequenced sub-tasks.
- Query Executor: Processes each sub-task generated by the Decomposer in optimal order, leveraging specialized handlers for different query types.
Configuration
Creating a Configuration Object
use Quellabs\ObjectQuel\Configuration; // Create a new configuration object $config = new Configuration();
Setting Database Connection
You have multiple options for configuring the database connection:
Option 1: Using individual parameters
$config->setDatabaseParams( 'mysql', // Database driver 'localhost', // Host 'my_database', // Database name 'db_user', // Username 'db_password', // Password 3306, // Port (optional, default: 3306) 'utf8mb4' // Character set (optional, default: utf8mb4) );
Option 2: Using a DSN string
$config->setDsn('mysql://db_user:db_password@localhost:3306/my_database?encoding=utf8mb4');
Option 3: Using an array
$config->setConnectionParams([ 'driver' => 'mysql', 'host' => 'localhost', 'database' => 'my_database', 'username' => 'db_user', 'password' => 'db_password', 'port' => 3306, 'encoding' => 'utf8mb4' ]);
Setting Entity Information
// Set the base namespace for entities // This is used when generating new entities through sculpt $config->setEntityNamespace('App\\Entity'); // Set the entity path (directory where entities reside) $config->setEntityPath(__DIR__ . '/src/Entity');
Configuring Proxies for Lazy Loading
// Set the directory where proxy classes will be stored $config->setProxyDir(__DIR__ . '/var/cache/proxies'); // Set the namespace for generated proxy classes $config->setProxyNamespace('App\\Proxies');
Important: Without proper proxy configuration, proxies will be generated dynamically at runtime, significantly impacting performance.
Configuring Metadata Caching
// Enable metadata caching $config->setUseMetadataCache(true); // Set where metadata cache will be stored $config->setMetadataCachePath(__DIR__ . '/var/cache/metadata');
Creating the EntityManager
use Quellabs\ObjectQuel\EntityManager; // Create the EntityManager with your configuration $entityManager = new EntityManager($config);
Working with Entities
Entity Retrieval
ObjectQuel provides three ways to retrieve entities:
1. Using find()
// Find entity by primary key $entity = $entityManager->find(\App\Entity\ProductEntity::class, 23);
2. Using findBy()
// Find entities matching criteria $entities = $entityManager->findBy(\App\Entity\ProductEntity::class, [ 'name' => 'Widget', 'price' => 19.99 ]);
3. Using a query
// Complex query using ObjectQuel language $results = $entityManager->executeQuery(" range of p is App\\Entity\\ProductEntity retrieve (p) where p.productId = :productId ", [ 'productId' => 1525 ]); foreach($results as $row) { echo $row['p']->getName(); }
Entity Creation
Entities are recognized by the @Orm\Table
annotation:
/** * Class ProductEntity * @Orm\Table(name="products") */ class ProductEntity { /** * @Orm\Column(name="product_id", type="integer", length=11, primary_key=true) * @Orm\PrimaryKeyStrategy(strategy="identity") */ private int $productId; // Properties and methods... }
Column Annotation Properties
Each database/entity property is marked by an @Orm\Column annotation. This annotation supports the following parameters:
Parameter | Description | Options/Format |
---|---|---|
name | The database column name | Required |
type | The data type | 'integer', 'string', 'char', 'text', 'datetime', etc. |
limit | The maximum column length | Only relevant for string types |
primary_key | Define this as a primary key column | true or false |
default | Default value when database column is NULL | Value |
unsigned | For unsigned values | true (unsigned) or false (signed, default) |
nullable | Allow NULL values in the database | true (allow NULL) or false (non-NULL required, default) |
Primary Key Strategies
For primary key properties, you can apply the @Orm\PrimaryKeyStrategy annotation to define how key values are generated. ObjectQuel supports the following strategies:
Strategy | Description |
---|---|
identity | Automatically increments values (default strategy) |
uuid | Generates a unique UUID for each new record |
sequence | Uses a select query to determine the next value in the sequence |
Primary Key Properties and Nullability
While primary keys are defined as NOT NULL in the database (a fundamental requirement for primary keys),
in PHP entity classes they should be declared as nullable types (?int
, ?string
, etc.). This approach properly
represents the state of new, unsaved entities that don't yet have ID values assigned:
/** * @Orm\Column(name="product_id", type="integer", limit=11, primary_key=true) * @Orm\PrimaryKeyStrategy(strategy="identity") */ private ?int $productId = null; // Nullable in PHP, NOT NULL in database
This pattern is especially important for identity (auto-increment) primary keys, as new entities won't have an ID until after they're persisted to the database. ObjectQuel's entity manager uses this nullability to determine whether an entity is new and requires an INSERT rather than an UPDATE operation.
The ObjectQuel Language
ObjectQuel draws inspiration from QUEL, a pioneering database query language developed in the 1970s for the Ingres DBMS at UC Berkeley (later acquired by Oracle). While SQL became the industry standard, QUEL's elegant approach to querying has been adapted here for modern entity-based programming:
- Entity-Centric: Works with domain entities instead of database tables
- Intuitive Syntax: Uses
RETRIEVE
instead ofSELECT
for more natural data extraction - Semantic Aliasing: Defines aliases with
range of x is y
(similar toFROM y AS x
in SQL) to create a more readable data scope - Object-Oriented: References entity properties directly instead of database columns, maintaining your domain language
- Relationship Traversal: Simplifies complex data relationships through intuitive path expressions
While ObjectQuel ultimately translates to SQL, implementing our own query language provides significant advantages. The abstraction layer allows ObjectQuel to:
- Express complex operations with elegant, developer-friendly syntax (e.g.,
productId = /^a/
instead of SQL's more verboseproductId REGEXP('^a')
) - Intelligently optimize database interactions by splitting operations into multiple efficient SQL queries when needed
- Perform additional post-processing operations not possible in SQL alone, such as seamlessly joining traditional database data with external JSON sources
This approach delivers both a more intuitive developer experience and capabilities that extend beyond standard SQL, all while maintaining a consistent, object-oriented interface.
Entity Property Selection
ObjectQuel provides flexibility in what data you retrieve. You can:
Retrieve entire entity objects:
$results = $entityManager->executeQuery(" range of p is App\\Entity\\ProductEntity retrieve (p) where p.productId = :productId ", [ 'productId' => 1525 ]); // Access the retrieved entity $product = $results[0]['p'];
Retrieve specific properties:
// Returns only the price property values $results = $entityManager->executeQuery(" range of p is App\\Entity\\ProductEntity retrieve (p.price) where p.productId = :productId ", [ 'productId' => 1525 ]); // Access the retrieved property value $price = $results[0]['p.price'];
Retrieve a mix of entities and properties:
// Returns product entities and just the name property from descriptions $results = $entityManager->executeQuery(" range of p is App\\Entity\\ProductEntity range of d is App\\Entity\\ProductDescriptionEntity via p.descriptions retrieve (p, d.productName) where p.productId = :productId sort by d.productName asc ", [ 'productId' => 1525 ]); // Access the mixed results $product = $results[0]['p']; $name = $results[0]['d.productName'];
Search Operations
ObjectQuel transforms database querying with its expressive, developer-friendly syntax that converts complex search operations into elegant, readable code.
Operation | Example | Description |
---|---|---|
Exact match | main.name = "xyz" |
Exact value match |
Starts with | main.name = "xyz*" |
Starts with "xyz" |
Pattern | main.name = "abc*xyz" |
Starts with "abc", ends with "xyz" |
Wildcard | main.name = "h?nk" |
Single character wildcard |
Regex | main.name = /^a/ |
Regular expression support |
Full-text | search(main.name, "banana cherry +pear -apple") |
Full-text search with weights |
Pagination
ObjectQuel supports pagination with the WINDOW operator:
range of p is App\\Entity\\ProductEntity range of d is App\\Entity\\ProductDescriptionEntity via d.productId = p.productId retrieve (p.productId) sort by d.productName window 1 using window_size 10
Entity Relationships
ObjectQuel supports five types of relationships:
1. OneToOne (owning-side)
/** * @Orm\OneToOne(targetEntity="CustomerEntity", inversedBy="customerId", relationColumn="customerInfoId", fetch="EAGER") */ private ?CustomerEntity $customer;
Parameter | Description |
---|---|
targetEntity | Target entity class |
inversedBy | Property in target entity for reverse mapping |
relationColumn | Column storing the foreign key |
fetch | Loading strategy ("EAGER" or "LAZY"; LAZY is default) |
2. OneToOne (inverse-side)
/** * @Orm\OneToOne(targetEntity="CustomerEntity", mappedBy="customerId", relationColumn="customerId") */ private ?CustomerEntity $customer;
Parameter | Description |
---|---|
targetEntity | Target entity class |
mappedBy | Property in target entity that holds the foreign key |
relationColumn | Column in current entity that corresponds to the relationship |
3. ManyToOne (owning-side)
/** * @Orm\ManyToOne(targetEntity="CustomerEntity", inversedBy="customerId", fetch="EAGER") * @Orm\RequiredRelation */ private ?CustomerEntity $customer;
Parameter | Description |
---|---|
targetEntity | Target entity class |
inversedBy | Property in target entity for reverse collection mapping |
fetch | Loading strategy ("EAGER" or "LAZY", optional. EAGER is the default) |
The annotation @Orm\RequiredRelation
indicates that the relation can be loaded using an INNER JOIN (rather than the default LEFT JOIN) because it's guaranteed to be present, which improves query performance.
4. OneToMany (inverse-side)
/** * @Orm\OneToMany(targetEntity="AddressEntity", mappedBy="customerId") * @var $addresses EntityCollection */ public $addresses;
Parameter | Description |
---|---|
targetEntity | Target entity class |
mappedBy | Property in target entity that contains the foreign key |
indexBy | Optional property to use as collection index |
5. ManyToMany
ManyToMany relationships are implemented as a specialized extension of OneToMany/ManyToOne relationships. To establish an effective ManyToMany relation:
- Apply the
@EntityBridge
annotation to your entity class that will serve as the junction table. - This annotation instructs the query processor to treat the entity as an intermediary linking table.
- When queries execute, the processor automatically traverses and loads the related ManyToOne associations defined within this bridge entity.
/** * Class ProductCategoryEntity * @Orm\Table(name="products_categories") * @Orm\EntityBridge */ class ProductCategoryEntity { // Properties defining the relationship }
The @Orm\EntityBridge
pattern extends beyond basic relationship mapping by offering several advanced capabilities:
- Store supplementary data within the junction table (relationship metadata, timestamps, etc.)
- Access and manipulate this contextual data alongside the primary relationship information
- Maintain comprehensive audit trails and relationship history between associated entities
Indexing
ObjectQuel provides powerful indexing capabilities through annotations at the entity level. These annotations allow you to define both regular and unique indexes directly in your entity classes, which will be automatically applied to your database during migrations.
Index Annotations
ObjectQuel supports two types of index annotations:
1. Regular Index (@Orm\Index)
Regular indexes improve query performance for columns frequently used in WHERE clauses, JOIN conditions, or sorting operations.
/** * @Orm\Table(name="products") * @Orm\Index(name="idx_product_category", columns={"category_id"}) */ class ProductEntity { // Entity properties and methods... }
2. Unique Index (@Orm\UniqueIndex)
Unique indexes ensure data integrity by preventing duplicate values in the specified columns, while also providing performance benefits.
/** * @Orm\Table(name="users") * @Orm\UniqueIndex(name="idx_unique_email", columns={"email"}) */ class UserEntity { // Entity properties and methods... }
Index Annotation Parameters
Both index annotations support the following parameters:
Parameter | Description | Required |
---|---|---|
name | Unique identifier for the index in the database | Yes |
columns | Array of column names to be included in the index | Yes |
Composite Indexes
You can create indexes on multiple columns to optimize queries that filter or sort by a combination of fields:
/** * @Orm\Table(name="orders") * @Orm\Index(name="idx_customer_date", columns={"customer_id", "order_date"}) */ class OrderEntity { // Entity properties and methods... }
Example Usage
Here's an example of how to use both index types on an entity:
/** * @Orm\Table(name="hamster") * @Orm\Index(name="idx_hamster_search", columns={"name", "color"}) * @Orm\UniqueIndex(name="idx_unique_code", columns={"registrationCode"}) */ class HamsterEntity { /** * @Orm\Column(name="id", type="integer", length=11, primary_key=true) * @Orm\PrimaryKeyStrategy(strategy="identity") */ private int $id; /** * @Orm\Column(name="name", type="string", length=100) */ private string $name; /** * @Orm\Column(name="color", type="string", length=50) */ private string $color; /** * @Orm\Column(name="registration_code", type="string", length=20) */ private string $registrationCode; // Getters and setters... }
Integration with Migrations
The index annotations are fully integrated with ObjectQuel's migration system. When you run the make:migrations
command, the system will:
- Analyze your entity annotations to identify index definitions
- Compare these definitions with the current database schema
- Generate appropriate migration scripts to add, modify, or remove indexes
- Apply these changes when migrations are executed
Example migration generated from index annotations:
<?php use Phinx\Migration\AbstractMigration; class EntitySchemaMigration20250515124653 extends AbstractMigration { /** * This migration was automatically generated by ObjectQuel * * More information on migrations is available on the Phinx website: * https://book.cakephp.org/phinx/0/en/migrations.html */ public function up(): void { $this->table('hamster', ['id' => false, 'primary_key' => ['id']]) ->addColumn('id', 'integer', ['limit' => 11, 'null' => false, 'signed' => false, 'identity' => true]) ->addColumn('woopie', 'string', ['limit' => 255, 'null' => false, 'signed' => true]) ->addIndex(['woopie'], ['name' => 'xyz', 'unique' => true]) ->create(); } public function down(): void { $this->table('hamster')->drop()->save(); } }
Performance Considerations
When defining indexes, consider these best practices:
- Create indexes only on columns frequently used in WHERE clauses, RANGE conditions, or SORT BY clauses
- Limit the number of indexes per table to minimize storage overhead and INSERT/UPDATE performance impact
- Place the most selective columns first in composite indexes
- Consider database-specific limitations on index sizes and types
Properly designed indexes can dramatically improve query performance while ensuring data integrity through unique constraints.
Saving and Persisting Data
Updating an Entity
// Retrieve an existing entity by its primary key // This queries the database for a ProductEntity with ID 10 // The entity is immediately loaded and tracked by the EntityManager // IMPORTANT: If no entity with ID 10 exists, this will return NULL, so error handling may be needed $entity = $entityManager->find(ProductEntity::class, 10); // Update entity property value // Modifies the text property/field of the retrieved entity // The EntityManager automatically tracks this change since the entity was loaded via find() $entity->setText("Updated description"); // Register the entity with the EntityManager's identity map // NOTE: This call is optional in this case because entities retrieved via find() are automatically // tracked by the EntityManager. Including it may improve code readability by explicitly showing // which entities are being managed, especially in complex operations $entityManager->persist($entity); // Synchronize all pending changes with the database // This operation: // 1. Detects changes to managed entities (the text field modification in this case) // 2. Executes necessary SQL statements (UPDATE in this case) // 3. Commits the transaction // 4. Clears the identity map (tracking information) // After flush(), the EntityManager no longer knows about previously managed entities $entityManager->flush();
Adding a New Entity
// Create a new entity instance // This instantiates a fresh entity object in memory without persisting it to the database yet $entity = new ProductEntity(); // Set entity property value // At this point, the change exists only in memory $entity->setText("New product description"); // Register the entity with the EntityManager's identity map // This tells the EntityManager to start tracking changes for this entity // IMPORTANT: This step is mandatory in ObjectQuel, unlike some other ORMs that automatically track new entities // Without this call, the entity would not be saved to the database during flush operations $entityManager->persist($entity); // Synchronize all pending changes with the database // This operation: // 1. Executes all necessary SQL statements (INSERT in this case) // 2. Commits the transaction // 3. Clears the identity map (tracking information) // After flush(), the EntityManager no longer knows about previously managed entities $entityManager->flush();
Removing an Entity
// Retrieve an existing entity by its primary key // This queries the database for a ProductEntity with ID 1520 // The entity is immediately loaded and tracked by the EntityManager // If no entity with ID 1520 exists, this will return NULL, so error handling may be needed $entity = $entityManager->find(ProductEntity::class, 1520); // Mark the entity for removal // This schedules the entity for deletion when flush() is called // The entity is not immediately deleted from the database // NOTE: This only works for entities that are being tracked by the EntityManager $entityManager->remove($entity); // Synchronize all pending changes with the database // This operation: // 1. Executes necessary SQL statements (DELETE in this case) // 2. Commits the transaction // 3. Clears the identity map (tracking information) // After flush(), the EntityManager no longer tracks this entity, and the record is removed from the database $entityManager->flush();
Note: When removing entities, ObjectQuel does not automatically cascade deletions to related entities unless you've configured foreign keys in your database engine. If you want child entities to be removed when their parent is deleted, add the @Orm\Cascade annotation to the ManyToOne relationship as shown below:
use Quellabs\ObjectQuel\Annotations\Orm; class Child { /** * @Orm\ManyToOne(targetEntity="Parent") * @Orm\Cascade(operations={"remove"}) */ private $parent; // ... }This tells ObjectQuel to find and remove all child entities that reference the deleted parent through their ManyToOne relationship.
Using Repositories
ObjectQuel provides a flexible approach to the Repository pattern through its optional Repository
base class. Unlike some ORMs that mandate repository usage, ObjectQuel makes repositories entirely optional—giving you the freedom to organize your data access layer as you prefer.
Repository Pattern Benefits
The Repository pattern creates an abstraction layer between your domain logic and data access code, providing several advantages:
- Type Safety: Better IDE autocomplete and type hinting
- Code Organization: Centralizes query logic for specific entity types
- Business Logic: Encapsulates common data access operations
- Testability: Simplifies mocking for unit tests
- Query Reusability: Prevents duplication of common queries
Creating Custom Repositories
While you can work directly with the EntityManager, creating entity-specific repositories can enhance your application's structure:
use Quellabs\ObjectQuel\Repository; use Quellabs\ObjectQuel\ObjectQuel\QuelResult; class ProductRepository extends Repository { /** * Constructor - specify the entity this repository manages * @param EntityManager $entityManager The EntityManager instance */ public function __construct(EntityManager $entityManager) { parent::__construct($entityManager, ProductEntity::class); } /** * Find products below a certain price * @param float $maxPrice Maximum price threshold * @return array<ProductEntity> Matching products */ public function findBelowPrice(float $maxPrice): QuelResult { return $this->entityManager->executeQuery(" range of p is App\\Entity\\ProductEntity retrieve (p) where p.price < :maxPrice sort by p.price asc ", [ 'maxPrice' => $maxPrice ]); } }
Using Repositories in Your Application
Once you've defined your repositories, you can integrate them into your application:
// Create the repository $productRepository = new ProductRepository($entityManager); // Use repository methods $affordableProducts = $productRepository->findBelowPrice(29.99); // Still have access to built-in methods $specificProduct = $productRepository->find(1001); $featuredProducts = $productRepository->findBy(['featured' => true]);
SignalHub
ObjectQuel provides a robust event system that allows you to execute custom logic at specific points in an entity's lifecycle. This event system is powered by our SignalHub component, offering both standard ORM lifecycle hooks and the flexibility to create custom events.
Lifecycle Events Overview
Lifecycle events allow you to intercept and respond to key moments in an entity's persistence lifecycle, such as:
Lifecycle Event | Description | Timing |
---|---|---|
prePersist | Triggered before a new entity is inserted | Before INSERT query |
postPersist | Triggered after a new entity is inserted | After INSERT query |
preUpdate | Triggered before an existing entity is updated | Before UPDATE query |
postUpdate | Triggered after an existing entity is updated | After UPDATE query |
preDelete | Triggered before an entity is deleted | Before DELETE query |
postDelete | Triggered after an entity is deleted | After DELETE query |
Setting Up Lifecycle Callbacks
To enable lifecycle callbacks on an entity:
- Mark your entity class with the
@LifecycleAware
annotation - Add methods with the appropriate lifecycle annotations
use Quellabs\ObjectQuel\Annotations\Orm\Table; use Quellabs\ObjectQuel\Annotations\Orm\LifecycleAware; use Quellabs\ObjectQuel\Annotations\Orm\PrePersist; use Quellabs\ObjectQuel\Annotations\Orm\PostUpdate; /** * @Table(name="products") * @LifecycleAware */ class ProductEntity { /** * @Orm\Column(name="created_at", type="datetime", nullable=true) */ private ?\DateTime $createdAt = null; /** * @Orm\Column(name="updated_at", type="datetime", nullable=true) */ private ?\DateTime $updatedAt = null; /** * @PrePersist */ public function setCreatedAt(): void { $this->createdAt = new \DateTime(); } /** * @PostUpdate */ public function setUpdatedAt(): void { $this->updatedAt = new \DateTime(); } // Other entity properties and methods... }
Once configured, these lifecycle methods will automatically be called at the appropriate times during the entity's lifecycle without any additional code required in your application logic.
Common Use Cases for Lifecycle Events
Some popular applications of lifecycle events include:
- Timestamp Management: Automatically set created/updated timestamps
- Value Generation: Generate UUIDs, slugs, or other derived values
- Validation: Perform complex validation before persisting data
- Cache Invalidation: Clear caches when entities change
- Logging: Record changes for audit trails
- Related Entity Updates: Update or validate related entities
- External System Synchronization: Push changes to external services
Listening to Built-in Lifecycle Signals
You can listen to standard entity lifecycle events without modifying your entities:
use Quellabs\SignalHub\SignalHubLocator; // Get SignalHub instance $signalHub = SignalHubLocator::getInstance(); // Connect a custom handler to the prePersist signal $signalHub->getSignal('orm.prePersist')->connect(function(object $entity) { // This will be called for all entities before they're persisted if ($entity instanceof ProductEntity) { // Do something specific for products logProductChange($entity); } });
Creating and Using Custom Signals
You can also define your own custom signals for domain-specific events:
use Quellabs\SignalHub\HasSignals; class ProductService { use HasSignals; public function __construct() { // Define a custom signal with parameter types $this->createSignal('productPriceChanged', ['ProductEntity', 'float', 'float']); // Register with SignalHub (optional) $this->registerWithHub(); } public function updatePrice(ProductEntity $product, float $newPrice): void { $oldPrice = $product->getPrice(); $product->setPrice($newPrice); // Emit the signal when price changes $this->emit('productPriceChanged', $product, $oldPrice, $newPrice); } }
SignalHub supports wildcard patterns for connecting to multiple signals:
// Connect to all signals that start with "product" $signalHub->getSignal('product*')->connect(function($entity) { // This will be called for all product-related signals recordProductActivity($entity); });
You can also specify priorities when connecting handlers to ensure they execute in the desired order:
// Higher priority handlers execute first $signalHub->getSignal('orm.prePersist')->connect($highPriorityHandler, null, 100); $signalHub->getSignal('orm.prePersist')->connect($normalPriorityHandler, null, 0); $signalHub->getSignal('orm.prePersist')->connect($lowPriorityHandler, null, -100);
Performance Considerations
While lifecycle events offer great flexibility, they can impact performance if overused. Keep these guidelines in mind:
- Lifecycle methods should be lightweight and fast
- Use PostPersist/PostUpdate for heavy operations when possible
- Consider batching operations in postFlush for better performance
- Enable eager loading for entities needed in lifecycle callbacks
When properly implemented, lifecycle events provide a clean, aspect-oriented approach to cross-cutting concerns while maintaining separation of your core domain logic from peripheral concerns like logging, validation, and data transformation.
Utility Tools
ObjectQuel provides a powerful utility tool called sculpt
that streamlines
the creation of entities in your application. This interactive CLI tool guides you
through a structured process, automatically generating properly formatted entity classes
with all the necessary components.
Initialization
Before using the sculpt
tool, create an objectquel-cli-config.php
configuration file in your project's root directory (where your composer.json
file is located). This file must include your database credentials, entity namespace, entity path, and migration path.
For convenience, ObjectQuel provides an objectquel-cli-config.php.example
file that you can copy and customize with your specific settings. The CLI tools require this configuration file to function properly.
Automatic Entity Generation
To create a new entity, run the following command in your terminal:
php bin/sculpt make:entity
When you execute this command, the sculpt
tool will:
- Prompt for entity name - Enter a descriptive name for your entity (e.g., "User", "Product", "Order")
- Define properties - Add fields with their respective data types (string, integer, boolean, etc.)
- Establish relationships - Define connections to other entities (One-to-One, One-to-Many, etc.)
- Generate accessors - Create getters and setters for your properties
Creating Entities from Database Tables
To generate an entity from an existing database table, run this command in your terminal:
php bin/sculpt make:entity-from-table
When executed, the sculpt tool will prompt you to select a table name and automatically create a properly structured entity class based on that table's schema.
Generating Database Migrations
To create migrations for entity changes, use this command:
php bin/sculpt make:migrations
When executed, the sculpt tool analyzes differences between your entity definitions and the current database schema. It then automatically generates a migration file containing the necessary SQL statements to synchronize your database with your entities.
Note: The system uses CakePHP's Phinx as its migration engine. All generated migrations follow the Phinx format and can be executed using standard Phinx commands.
Generating Migrations for Index Changes
When you add or modify @Orm\Index
or @Orm\UniqueIndex
annotations to your entities, the make:migrations
command will automatically detect these changes and include them in the generated migration files.
php bin/sculpt make:migrations
This will produce migrations for index changes similar to:
<?php use Phinx\Migration\AbstractMigration; class AddProductIndices extends AbstractMigration { public function change() { $table = $this->table('products'); // Add a regular index $table->addIndex(['category_id'], [ 'name' => 'idx_product_category', 'unique' => false, ]); $table->save(); } }
Running Database Migrations
Database migrations allow you to manage and version your database schema changes alongside your application code. This guide covers how to run, rollback, and manage migrations effectively.
Applying Migrations
To apply all pending migrations to your database:
php bin/sculpt quel:migrate
This command will:
- Identify all pending migrations that haven't been applied yet
- Execute them in sequence (ordered by creation date)
- Apply the database schema changes defined in your entity annotations
- Record the migration in the migrations table (
phinxlog
) to track what's been applied
Rolling Back Migrations
If you need to undo the most recent migration:
php bin/sculpt quel:migrate --rollback
To roll back multiple migrations at once, specify the number to rollback:
php bin/sculpt quel:migrate --rollback --steps=3
Command Help
For a complete list of migration options and detailed help:
php bin/sculpt help quel:migrate
Technology Stack
Note: Our migration system is a wrapper around Phinx, a powerful database migration tool that provides a robust foundation for schema management. While our commands simplify common migration tasks, some advanced Phinx features like database seeding and migration breakpoints are not available through our wrapper. If you need these advanced capabilities, you can use Phinx directly. For more information on using Phinx's full feature set, refer to the Phinx documentation.
Query Optimization
Query Flags
ObjectQuel supports query flags for optimization, starting with the '@' symbol:
@InValuesAreFinal
: Optimizes IN() functions for primary keys by eliminating verification queries
Important Notes
- Proxy cache directories must be writable by the application
- For best performance in production, enable proxy and metadata caching
Note: When proxy path and namespace settings are not configured, the system generates proxies on-the-fly during runtime. This approach significantly reduces performance and can cause noticeable slowdowns in your application. For optimal performance, always configure both the proxy path and namespace in your application settings.
License
ObjectQuel is released under the MIT License.
MIT License
Copyright (c) 2024-2025 Quellabs
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.