WithDatastoreHandlerMethods Trait
The WithDatastoreHandlerMethods trait provides the actual implementation of all standard datastore handler methods. When you extend IdentifiableDatabaseDatastoreHandler and use this trait, you get complete CRUD functionality with zero boilerplate.
This trait is the workhorse that powers database handlers—it contains all the query building, caching, event broadcasting, and data conversion logic.
What It Provides
The trait implements:
- CRUD operations —
find(),get(),save(),delete() - Query building — constructs SQL queries with proper escaping
- WHERE clause support —
where()method returning query builder - Counting —
count()method for efficient record counting - Automatic caching — caches reads, invalidates on writes
- Event broadcasting — fires events on mutations
- Error handling — catches and logs database errors
Basic Usage
<?php
use PHPNomad\Database\Abstracts\IdentifiableDatabaseDatastoreHandler;
use PHPNomad\Database\Traits\WithDatastoreHandlerMethods;
class PostHandler extends IdentifiableDatabaseDatastoreHandler
{
use WithDatastoreHandlerMethods;
public function __construct(
DatabaseServiceProvider $serviceProvider,
PostsTable $table,
PostAdapter $adapter,
TableSchemaService $tableSchemaService
) {
$this->model = Post::class;
$this->table = $table;
$this->modelAdapter = $adapter;
$this->serviceProvider = $serviceProvider;
$this->tableSchemaService = $tableSchemaService;
}
}
That's all you need—the trait provides all CRUD methods automatically.
Implemented Methods
find(int $id): Model
What it does:
- Generates cache key from ID
- Checks cache for existing record
- On cache miss:
- Builds SELECT query with WHERE id = ?
- Executes query via QueryStrategy
- Converts result row to model via adapter
- Stores in cache
- Returns model
Throws: RecordNotFoundException if ID doesn't exist
Generated SQL:
SELECT * FROM wp_posts WHERE id = 123
get(array $args = []): iterable
What it does:
- Builds WHERE clause from
$argsarray - Constructs SELECT query
- Executes query
- Converts each row to model via adapter
- Returns iterable collection
Example args:
$args = [
'author_id' => 123,
'status' => 'published',
'limit' => 10,
'offset' => 20
];
Generated SQL:
SELECT * FROM wp_posts
WHERE author_id = 123 AND status = 'published'
LIMIT 10 OFFSET 20
save(Model $item): Model
What it does:
- Converts model to array via adapter
- Checks if model has primary key:
- If NO key → INSERT new record
- If HAS key → UPDATE existing record
- Executes query via QueryStrategy
- Invalidates cache for this record
- Broadcasts event:
RecordCreatedfor INSERTRecordUpdatedfor UPDATE
- Returns model with ID populated
INSERT SQL:
INSERT INTO wp_posts (title, content, author_id, published_date)
VALUES ('Title', 'Content', 123, '2024-01-01 12:00:00')
UPDATE SQL:
UPDATE wp_posts
SET title = 'New Title', content = 'New Content'
WHERE id = 123
delete(Model $item): void
What it does:
- Extracts primary key from model
- Builds DELETE query
- Executes query
- Invalidates cache for this record
- Broadcasts
RecordDeletedevent
Generated SQL:
DELETE FROM wp_posts WHERE id = 123
where(): DatastoreWhereQuery
What it does: Returns a query builder instance configured for the handler's table.
Returns: Object with fluent API:
equals(field, value)greaterThan(field, value)lessThan(field, value)in(field, ...values)like(field, pattern)orderBy(field, direction)limit(count)offset(count)getResults()
Example:
$posts = $handler
->where()
->equals('status', 'published')
->greaterThan('view_count', 100)
->orderBy('published_date', 'DESC')
->limit(10)
->getResults();
count(array $args = []): int
What it does:
- Builds WHERE clause from
$args - Constructs SELECT COUNT(*) query
- Executes query
- Returns integer count
Generated SQL:
SELECT COUNT(*) FROM wp_posts
WHERE status = 'published'
Caching Implementation
The trait integrates with CacheableService:
Cache Keys
Single record:
// Cache key: posts:123
$cacheKey = $this->table->getTableName() . ':' . $id;
List queries:
// Cache key: posts:list:md5(serialize($args))
$cacheKey = $this->table->getTableName() . ':list:' . md5(serialize($args));
Cache Invalidation
On save() or delete():
// Clear single record cache
$this->serviceProvider->cacheableService->forget(['id' => $id]);
// Clear all list caches for this table
$this->serviceProvider->cacheableService->forgetMatching(
$this->table->getTableName() . ':list:*'
);
Event Broadcasting
The trait broadcasts standard events:
RecordCreated
Fired after successful INSERT:
$this->serviceProvider->eventStrategy->broadcast(
new RecordCreated(
table: $this->table->getTableName(),
model: $savedModel
)
);
RecordUpdated
Fired after successful UPDATE:
$this->serviceProvider->eventStrategy->broadcast(
new RecordUpdated(
table: $this->table->getTableName(),
model: $savedModel
)
);
RecordDeleted
Fired after successful DELETE:
$this->serviceProvider->eventStrategy->broadcast(
new RecordDeleted(
table: $this->table->getTableName(),
model: $deletedModel
)
);
Query Building Implementation
The trait uses QueryBuilder and ClauseBuilder from the service provider:
Building SELECT Queries
protected function buildSelectQuery(array $args): string
{
$clause = $this->serviceProvider->clauseBuilder
->useTable($this->table);
// Add WHERE conditions from args
foreach ($args as $field => $value) {
if ($field === 'limit' || $field === 'offset') {
continue; // Handle separately
}
$clause->where($field, '=', $value);
}
$query = $this->serviceProvider->queryBuilder
->select('*')
->from($this->table)
->where($clause);
// Handle pagination
if (isset($args['limit'])) {
$query->limit($args['limit']);
}
if (isset($args['offset'])) {
$query->offset($args['offset']);
}
return $query->build();
}
Error Handling
The trait includes comprehensive error handling:
RecordNotFoundException
Thrown when find() doesn't locate a record:
if (!$row) {
throw new RecordNotFoundException(
"Record not found: {$this->table->getTableName()}:{$id}"
);
}
Database Errors
Caught and logged:
try {
$result = $this->serviceProvider->queryStrategy->execute($sql);
} catch (DatabaseException $e) {
$this->serviceProvider->loggerStrategy->error(
'Database query failed',
[
'table' => $this->table->getTableName(),
'query' => $sql,
'error' => $e->getMessage()
]
);
throw $e;
}
Overriding Trait Methods
You can override any trait method to customize behavior:
Override save() with Custom Logic
class PostHandler extends IdentifiableDatabaseDatastoreHandler
{
use WithDatastoreHandlerMethods {
save as private traitSave; // Rename trait method
}
public function save(Model $item): Model
{
// Custom pre-save logic
$this->validatePost($item);
// Call trait's save
$result = $this->traitSave($item);
// Custom post-save logic
$this->updateSearchIndex($result);
return $result;
}
private function validatePost(Model $post): void
{
if (empty($post->title)) {
throw new ValidationException('Title required');
}
}
}
Override find() with Custom Caching
class PostHandler extends IdentifiableDatabaseDatastoreHandler
{
use WithDatastoreHandlerMethods;
public function find(int $id): Model
{
// Custom cache key
$cacheKey = "posts:full:{$id}";
return $this->serviceProvider->cacheableService->getWithCache(
operation: 'find',
context: ['key' => $cacheKey],
callback: function() use ($id) {
// Execute query
$sql = $this->buildFindQuery($id);
$row = $this->serviceProvider->queryStrategy->querySingle($sql);
if (!$row) {
throw new RecordNotFoundException("Post {$id} not found");
}
return $this->modelAdapter->toModel($row);
}
);
}
}
Required Properties
The trait expects these properties to be set by your handler:
protected string $model; // Model class name
protected Table $table; // Table definition
protected ModelAdapter $modelAdapter; // Model adapter
protected DatabaseServiceProvider $serviceProvider; // Service provider
protected TableSchemaService $tableSchemaService; // Schema service
If any are missing, trait methods will fail with errors.
Best Practices
Always Use the Trait
// ✅ GOOD: use trait
class PostHandler extends IdentifiableDatabaseDatastoreHandler
{
use WithDatastoreHandlerMethods;
}
// ❌ BAD: manual implementation
class PostHandler extends IdentifiableDatabaseDatastoreHandler
{
public function find(int $id): Model
{
// Reimplementing what the trait already does
}
}
Override Only When Necessary
// ✅ GOOD: override for specific needs
public function save(Model $item): Model
{
$this->logSaveAttempt($item);
return $this->traitSave($item);
}
// ❌ BAD: override without adding value
public function save(Model $item): Model
{
return $this->traitSave($item); // No customization
}
Use Proper Method Renaming
// ✅ GOOD: rename trait method to avoid conflicts
use WithDatastoreHandlerMethods {
save as private traitSave;
}
public function save(Model $item): Model
{
return $this->traitSave($item);
}
// ❌ BAD: call parent (doesn't work with traits)
public function save(Model $item): Model
{
return parent::save($item); // Error!
}
What's Next
- IdentifiableDatabaseDatastoreHandler — the base class that uses this trait
- Database Handlers Introduction — handler architecture overview
- Query Building — understanding query construction
- Caching and Events — how caching and events work