Skip to content

Commit 813ac59

Browse files
Feature: Rebuilt relationship structure (#32)
1 parent 0836e17 commit 813ac59

11 files changed

+2239
-71
lines changed

README.md

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,173 @@ class Breakfast_Model extends Model {
328328
}
329329
```
330330

331+
## Model Relationships
332+
333+
Models can define relationships to other models, similar to how properties are defined. Relationships support lazy loading and caching.
334+
335+
### Defining Relationships
336+
337+
Relationships can be defined using either shorthand syntax or the fluent `ModelRelationshipDefinition` API:
338+
339+
#### Using shorthand syntax:
340+
341+
```php
342+
namespace Boomshakalaka\Whatever;
343+
344+
use Boomshakalaka\StellarWP\Models\Model;
345+
use Boomshakalaka\StellarWP\Models\ValueObjects\Relationship;
346+
347+
class Product_Model extends Model {
348+
/**
349+
* @inheritDoc
350+
*/
351+
protected static $relationships = [
352+
'category' => Relationship::BELONGS_TO,
353+
'reviews' => Relationship::HAS_MANY,
354+
'tags' => Relationship::MANY_TO_MANY,
355+
];
356+
357+
/**
358+
* Define how to load the category relationship.
359+
*/
360+
protected function category() {
361+
return Category_Model::query()->where('id', $this->category_id);
362+
}
363+
364+
/**
365+
* Define how to load the reviews relationship.
366+
*/
367+
protected function reviews() {
368+
return Review_Model::query()->where('product_id', $this->id);
369+
}
370+
371+
/**
372+
* Define how to load the tags relationship.
373+
*/
374+
protected function tags() {
375+
return Tag_Model::query()
376+
->select('tags.*')
377+
->join('product_tags', 'product_tags.tag_id', 'tags.id')
378+
->where('product_tags.product_id', $this->id);
379+
}
380+
}
381+
```
382+
383+
#### Using relationship definitions for more control:
384+
385+
```php
386+
namespace Boomshakalaka\Whatever;
387+
388+
use Boomshakalaka\StellarWP\Models\Model;
389+
use Boomshakalaka\StellarWP\Models\ModelRelationshipDefinition;
390+
391+
class Product_Model extends Model {
392+
/**
393+
* @inheritDoc
394+
*/
395+
protected static function relationships(): array {
396+
return [
397+
'category' => (new ModelRelationshipDefinition('category'))
398+
->belongsTo(),
399+
'reviews' => (new ModelRelationshipDefinition('reviews'))
400+
->hasMany(),
401+
'tags' => (new ModelRelationshipDefinition('tags'))
402+
->manyToMany()
403+
->disableCaching(), // Don't cache this relationship
404+
];
405+
}
406+
407+
// Define relationship loaders as above...
408+
}
409+
```
410+
411+
### Relationship Types
412+
413+
Five relationship types are available:
414+
415+
- `Relationship::HAS_ONE` - Model has one related model
416+
- `Relationship::HAS_MANY` - Model has many related models
417+
- `Relationship::BELONGS_TO` - Model belongs to another model
418+
- `Relationship::BELONGS_TO_MANY` - Model belongs to many related models
419+
- `Relationship::MANY_TO_MANY` - Many-to-many relationship
420+
421+
### Accessing Relationships
422+
423+
Relationships are loaded lazily when accessed as properties:
424+
425+
```php
426+
$product = Product_Model::find(1);
427+
428+
// First access loads from database and caches result
429+
$category = $product->category;
430+
431+
// Subsequent accesses use cached value (if caching enabled)
432+
$category = $product->category; // No additional query
433+
434+
// Access multiple relationship
435+
$reviews = $product->reviews; // Returns array of Review_Model instances
436+
```
437+
438+
### Relationship Caching
439+
440+
By default, relationships are cached after the first load. You can control caching behavior:
441+
442+
```php
443+
class Product_Model extends Model {
444+
protected static function relationships(): array {
445+
return [
446+
// Cached (default)
447+
'category' => (new ModelRelationshipDefinition('category'))
448+
->belongsTo(),
449+
450+
// Not cached - always loads fresh
451+
'stock' => (new ModelRelationshipDefinition('stock'))
452+
->hasOne()
453+
->disableCaching(),
454+
];
455+
}
456+
}
457+
```
458+
459+
### Managing Relationship Cache
460+
461+
Models provide methods to manage relationship caching:
462+
463+
```php
464+
$product = Product_Model::find(1);
465+
466+
// Manually set a cached relationship value
467+
$product->setCachedRelationship('category', $newCategory);
468+
469+
// Clear a specific relationship cache
470+
$product->purgeRelationship('category');
471+
$category = $product->category; // Reloads from database
472+
473+
// Clear all relationship caches
474+
$product->purgeRelationshipCache();
475+
```
476+
477+
### Customizing Relationship Loading
478+
479+
Override the `fetchRelationship()` method to customize how relationships are loaded:
480+
481+
```php
482+
class Product_Model extends Model {
483+
/**
484+
* Custom relationship loading logic.
485+
*/
486+
protected function fetchRelationship(string $key) {
487+
// Add custom logic before loading
488+
if ($key === 'category' && !$this->category_id) {
489+
return null;
490+
}
491+
492+
// Default loading behavior
493+
return parent::fetchRelationship($key);
494+
}
495+
}
496+
```
497+
331498
## Attribute validation
332499

333500
Sometimes it would be helpful to validate attributes that are set in the model. To do that, you can create `validate_*()`

UPGRADING.md

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -460,7 +460,87 @@ $product = Product_Model::query()->where('id', 1)->get();
460460
$products = Product_Model::query()->where('active', 1)->getAll();
461461
```
462462

463-
### 9. Relationship Cache Control
463+
### 9. Enhanced Relationship System
464+
465+
The relationship system has been completely redesigned to work like the property system with better type safety and flexibility.
466+
467+
#### Relationship Definitions
468+
469+
You can now define relationships using `ModelRelationshipDefinition` for more control:
470+
471+
```php
472+
use StellarWP\Models\ModelRelationshipDefinition;
473+
use StellarWP\Models\ValueObjects\Relationship;
474+
475+
class Product_Model extends Model {
476+
// Shorthand syntax (still supported)
477+
protected static $relationships = [
478+
'category' => Relationship::BELONGS_TO,
479+
'reviews' => Relationship::HAS_MANY,
480+
];
481+
482+
// Or use the fluent API for more control
483+
protected static function relationships(): array {
484+
return [
485+
'category' => (new ModelRelationshipDefinition('category'))
486+
->belongsTo(),
487+
'reviews' => (new ModelRelationshipDefinition('reviews'))
488+
->hasMany()
489+
->disableCaching(), // Disable caching for this relationship
490+
'tags' => (new ModelRelationshipDefinition('tags'))
491+
->manyToMany(),
492+
];
493+
}
494+
495+
// Define relationship loaders
496+
protected function category() {
497+
return Category_Model::query()->where('id', $this->category_id);
498+
}
499+
500+
protected function reviews() {
501+
return Review_Model::query()->where('product_id', $this->id);
502+
}
503+
504+
protected function tags() {
505+
return Tag_Model::query()
506+
->join('product_tags', 'product_tags.tag_id', 'tags.id')
507+
->where('product_tags.product_id', $this->id);
508+
}
509+
}
510+
```
511+
512+
#### Relationship Type Value Object
513+
514+
The `Relationship` class is now a proper value object with instance caching (flyweight pattern):
515+
516+
```php
517+
use StellarWP\Models\ValueObjects\Relationship;
518+
519+
// Factory methods return cached instances
520+
$hasMany1 = Relationship::HAS_MANY();
521+
$hasMany2 = Relationship::HAS_MANY();
522+
$hasMany1 === $hasMany2; // true - same instance
523+
524+
// Create from string
525+
$relationship = Relationship::from('has-many');
526+
527+
// Type checking methods
528+
$relationship->isHasMany(); // true
529+
$relationship->isSingle(); // false
530+
$relationship->isMultiple(); // true
531+
532+
// Get all relationship types
533+
$all = Relationship::all(); // Returns array of all 5 relationship types
534+
```
535+
536+
Available relationship types:
537+
- `Relationship::HAS_ONE` - Single related model
538+
- `Relationship::HAS_MANY` - Multiple related models
539+
- `Relationship::BELONGS_TO` - Single parent model
540+
- `Relationship::BELONGS_TO_MANY` - Multiple parent models
541+
- `Relationship::MANY_TO_MANY` - Many-to-many relationship
542+
543+
#### Relationship Cache Control
464544

465545
New protected methods for managing relationship caching within your model subclasses:
466546

@@ -489,13 +569,23 @@ class Product_Model extends Model {
489569
// Clear all cached relationships
490570
$this->purgeRelationshipCache();
491571
}
572+
573+
// Override to customize relationship loading
574+
protected function fetchRelationship(string $key) {
575+
if ($key === 'category' && !$this->category_id) {
576+
return null; // No category to load
577+
}
578+
579+
return parent::fetchRelationship($key);
580+
}
492581
}
493582
```
494583

495584
Available methods:
496585
- `setCachedRelationship(string $key, $value)` - Manually set a cached relationship value
497586
- `purgeRelationship(string $key)` - Clear a specific relationship from cache
498587
- `purgeRelationshipCache()` - Clear all relationship caches
588+
- `fetchRelationship(string $key)` - Override to customize relationship loading logic
499589

500590
## Getting Help
501591

0 commit comments

Comments
 (0)