quellabs / objectquel
A sophisticated ORM system with a unique query language and streamlined architecture
Requires
- ext-curl: *
- ext-fileinfo: *
- ext-gd: *
- ext-json: *
- ext-mysqli: *
- ext-pdo: *
- cakephp/database: *
- quellabs/annotation-reader: ^1.
- quellabs/cache: ^1.
- quellabs/contracts: ^1.
- quellabs/dependency-injection: ^1.
- quellabs/sculpt: ^1.
- quellabs/signal-hub: ^1.
- quellabs/support: ^1.
- robmorgan/phinx: *
- softcreatr/jsonpath: *
This package is auto-updated.
Last update: 2025-08-13 14:18:42 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.
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 subtasks.
- Query Executor: Processes each subtask 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 |
Aggregate Functions
ObjectQuel supports built-in aggregate functions for data analysis:
Function | Description | Example |
---|---|---|
COUNT | Returns the count of rows | retrieve (COUNT(p.productId)) |
COUNTU | Returns the count of unique rows | retrieve (COUNTU(p.productId)) |
MIN | Returns the minimum value | retrieve (MIN(p.price)) |
MAX | Returns the maximum value | retrieve (MAX(p.price)) |
AVG | Returns the average value | retrieve (AVG(p.price)) |
AVGU | Returns the average of unique values | retrieve (AVGU(p.price)) |
SUM | Returns the sum of all values | retrieve (SUM(p.price)) |
Example using aggregate functions:
$results = $entityManager->executeQuery(" range of p is App\\Entity\\ProductEntity retrieve (COUNT(p.productId), MIN(p.price), MAX(p.price), AVG(p.price), SUM(p.price)) where p.category = :category ", [ 'category' => 'Electronics' ]); $productCount = $results[0]['COUNT(p.productId)']; $minPrice = $results[0]['MIN(p.price)']; $maxPrice = $results[0]['MAX(p.price)']; $avgPrice = $results[0]['AVG(p.price)']; $totalValue = $results[0]['SUM(p.price)'];
Pagination
ObjectQuel provides a developer-friendly pagination system that abstracts away the complexity of SQL's offset-based LIMIT clause. Instead of requiring developers to calculate offsets manually, ObjectQuel uses a page-based approach where you specify which page you want and how many items per page.
ObjectQuel supports two syntax styles for pagination:
- Standard syntax:
window 0 using window_size 10
- More verbose but explicit - Shorthand syntax:
window 0,10
- Compact and convenient
Both approaches are zero-indexed (page 0 is the first page) and functionally equivalent.
Standard Window Syntax
// Get first item range of p is App\\Entity\\ProductEntity retrieve (p.productId) window 0 using window_size 1
Shorthand Window Syntax
For simpler pagination, you can use the shorthand notation:
// Get page 0 (first 10 items) range of p is App\\Entity\\ProductEntity retrieve (p) window 0,10 // Get page 2 (items 21-30) range of p is App\\Entity\\ProductEntity retrieve (p) window 2,10
Entity Relationships
ObjectQuel supports five types of relationships:
1. OneToOne (owning-side)
/** * @Orm\OneToOne(targetEntity="CustomerEntity", inversedBy="profile", relationColumn="customerId", fetch="EAGER") */ private ?CustomerEntity $customer;
Parameter | Description |
---|---|
targetEntity | Target entity class |
inversedBy | Property in target entity for reverse mapping |
relationColumn | Column in current entity that corresponds to the relationship |
fetch | Loading strategy ("EAGER" or "LAZY"; LAZY is default) |
2. OneToOne (inverse-side)
/** * @Orm\OneToOne(targetEntity="CustomerEntity", mappedBy="profile", relationColumn="customerId") */ private ?CustomerEntity $customer;
Parameter | Description |
---|---|
targetEntity | Target entity class |
mappedBy | Property in target entity that references back to this entity |
relationColumn | Column in current entity that corresponds to the relationship |
3. ManyToOne (owning-side)
/** * @Orm\ManyToOne(targetEntity="CustomerEntity", inversedBy="addresses", 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. LAZY 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="customer") * @var $addresses EntityCollection */ public $addresses;
Parameter | Description |
---|---|
targetEntity | Target entity class |
mappedBy | Property in target entity that references back to this entity |
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... }
Saving and Persisting Data
Updating an Entity
// Retrieve an existing entity by its primary key $entity = $entityManager->find(ProductEntity::class, 10); // Update entity property value $entity->setText("Updated description"); // Register the entity with the EntityManager's identity map (optional for tracked entities) $entityManager->persist($entity); // Synchronize all pending changes with the database $entityManager->flush();
Adding a New Entity
// Create a new entity instance $entity = new ProductEntity(); // Set entity property value $entity->setText("New product description"); // Register the entity with the EntityManager's identity map (required for new entities) $entityManager->persist($entity); // Synchronize all pending changes with the database $entityManager->flush();
Removing an Entity
// Retrieve an existing entity by its primary key $entity = $entityManager->find(ProductEntity::class, 1520); // Mark the entity for removal $entityManager->remove($entity); // Synchronize all pending changes with 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.
Project Initialization
Before using the sculpt CLI tools for creating entities or running migrations, you need to initialize ObjectQuel configuration in your project. This sets up the configuration files that the sculpt CLI tool needs to connect to your database and know where to generate entities.
Note: These configuration files are used by the sculpt CLI tool and also by the Canvas PHP framework if you're using ObjectQuel within Canvas. For standalone ObjectQuel usage, you'll still need to create a Configuration object programmatically in your application code.
php vendor/bin/sculpt quel:init
When you execute this command, the sculpt
tool will:
- Create configuration directory - Sets up a
config/
directory in your project root if it doesn't exist - Generate database configuration - Creates
config/database.php
with your main database settings for sculpt to use - Provide setup guidance - Offers clear next steps to complete your sculpt CLI setup
What Gets Created
The initialization process creates configuration files used by both the sculpt CLI tool and the Canvas framework (if applicable):
- config/database.php: Database configuration file where sculpt reads your connection settings
After running the init command, you'll need to:
- Edit
config/database.php
to configure your database connection details for sculpt - Ensure sensitive data like passwords use environment variables for security
Important: If you're using ObjectQuel standalone (without Canvas), your application code will still need to create and configure the ObjectQuel Configuration object programmatically as shown in the Configuration section. Canvas automatically uses these config files to initialize ObjectQuel for you.
Automatic Entity Generation
To create a new entity, run the following command in your terminal:
php vendor/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 vendor/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 vendor/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 vendor/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 vendor/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 vendor/bin/sculpt quel:migrate --rollback
To roll back multiple migrations at once, specify the number to rollback:
php vendor/bin/sculpt quel:migrate --rollback --steps=3
Command Help
For a complete list of migration options and detailed help:
php vendor/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.