Skip to content

Commit 34d0767

Browse files
authored
Merge pull request #35 from stellarwp/feat/more-flexibility-on-defining-relationships-and-its-values
Adds more flexibility on definining relationships and its values
2 parents 06c01f6 + ea5a000 commit 34d0767

File tree

12 files changed

+215
-275
lines changed

12 files changed

+215
-275
lines changed

src/Models/Contracts/ModelPersistable.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ interface ModelPersistable extends Model {
1515
*
1616
* @param int $id
1717
*
18-
* @return Model
18+
* @return ?Model
1919
*/
20-
public static function find( $id ): Model;
20+
public static function find( $id ): ?Model;
2121

2222
/**
2323
* @since 1.0.0

src/Models/Model.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -488,7 +488,7 @@ protected function purgeRelationship( string $key ): void {
488488
*
489489
* @return void
490490
*/
491-
protected function setCachedRelationship( string $key, $value ): void {
491+
public function setCachedRelationship( string $key, $value ): void {
492492
$relationship = $this->relationshipCollection->get( $key );
493493

494494
if ( ! $relationship ) {

src/Models/ModelRelationship.php

Lines changed: 45 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ class ModelRelationship {
3232
/**
3333
* The relationship value.
3434
*
35-
* @var Model|list<Model>|null
35+
* @var mixed
3636
*/
3737
private $value;
3838

@@ -79,19 +79,56 @@ public function getKey(): string {
7979
public function getValue( callable $loader ) {
8080
// If caching is disabled, always load fresh
8181
if ( ! $this->definition->hasCachingEnabled() ) {
82-
return $loader();
82+
return $this->hydrate( $loader() );
8383
}
8484

8585
// If already loaded and caching is enabled, return cached value
8686
if ( $this->isLoaded ) {
87-
return $this->value;
87+
return $this->hydrate( $this->value );
8888
}
8989

9090
// Load and cache the value
9191
$this->setValue( $loader() );
92+
return $this->hydrate( $this->value );
93+
}
94+
95+
/**
96+
* Get the raw value of the relationship.
97+
*
98+
* @since 2.0.0
99+
*
100+
* @param callable():( Model|list<Model>|null ) $loader A callable that loads the relationship value.
101+
*
102+
* @return mixed
103+
*/
104+
public function getRawValue( callable $loader ) {
105+
$this->getValue( $loader );
92106
return $this->value;
93107
}
94108

109+
/**
110+
* Hydrate the relationship value.
111+
*
112+
* @since 2.0.0
113+
*
114+
* @param mixed $value The relationship value.
115+
*
116+
* @return Model|list<Model>|null
117+
*/
118+
private function hydrate( $value ) {
119+
if ( null === $value ) {
120+
return null;
121+
}
122+
123+
$hydrator = $this->definition->getHydrateWith();
124+
125+
if ( is_array( $value ) ) {
126+
return array_values( array_map( fn( $item ) => $hydrator( $item ), $value ) );
127+
}
128+
129+
return $hydrator( $value );
130+
}
131+
95132
/**
96133
* Returns whether the relationship has been loaded.
97134
*
@@ -121,22 +158,18 @@ public function purge(): void {
121158
* @throws InvalidArgumentException When the value is invalid.
122159
*/
123160
public function setValue( $value ): self {
124-
// Validate the value
125161
if ( $value !== null ) {
126-
if ( $this->definition->isSingle() && ! $value instanceof Model ) {
127-
throw new InvalidArgumentException( 'Single relationship value must be a Model instance or null.' );
162+
if ( $this->definition->isSingle() ) {
163+
$value = $this->definition->getValidateSanitizeRelationshipWith()( $value );
128164
}
129165

130166
if ( $this->definition->isMultiple() ) {
131167
if ( ! is_array( $value ) ) {
132-
throw new InvalidArgumentException( 'Multiple relationship value must be an array or null.' );
168+
Config::throwInvalidArgumentException( 'Multiple relationship value must be an array or null.' );
133169
}
134170

135-
foreach ( $value as $item ) {
136-
if ( ! $item instanceof Model ) {
137-
throw new InvalidArgumentException( 'Multiple relationship value must be an array of Model instances.' );
138-
}
139-
}
171+
$sanitizer = $this->definition->getValidateSanitizeRelationshipWith();
172+
$value = array_map( $sanitizer, $value );
140173
}
141174
}
142175

src/Models/ModelRelationshipDefinition.php

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,24 @@ class ModelRelationshipDefinition {
2323
*/
2424
private bool $cachingEnabled = true;
2525

26+
/**
27+
* The callable to hydrate the relationship with.
28+
*
29+
* @since 2.0.0
30+
*
31+
* @var ?callable
32+
*/
33+
private $hydrateWith = null;
34+
35+
/**
36+
* The callable to validate and sanitize the relationship with.
37+
*
38+
* @since 2.0.0
39+
*
40+
* @var ?callable
41+
*/
42+
private $validateSanitizeRelationshipWith = null;
43+
2644
/**
2745
* Whether the definition is locked. Once locked, the definition cannot be changed.
2846
*
@@ -79,6 +97,66 @@ public function belongsTo(): self {
7997
return $this;
8098
}
8199

100+
/**
101+
* Set the callable to hydrate the relationship with.
102+
*
103+
* @since 2.0.0
104+
*
105+
* @param callable $hydrateWith The callable to hydrate the relationship with.
106+
*/
107+
public function setHydrateWith( callable $hydrateWith ): self {
108+
$this->checkLock();
109+
110+
$this->hydrateWith = $hydrateWith;
111+
112+
return $this;
113+
}
114+
115+
/**
116+
* Get the callable to hydrate the relationship with.
117+
*
118+
* @since 2.0.0
119+
*
120+
* @return callable( mixed $value ): ( Model )
121+
*/
122+
public function getHydrateWith(): callable {
123+
// By default, it returns whats given.
124+
return $this->hydrateWith ?? static fn( $value ) => $value;
125+
}
126+
127+
/**
128+
* Set the callable to validate the relationship with.
129+
*
130+
* @since 2.0.0
131+
*
132+
* @param callable $validateSanitizeRelationshipWith The callable to validate the relationship with.
133+
*/
134+
public function setValidateSanitizeRelationshipWith( callable $validateSanitizeRelationshipWith ): self {
135+
$this->checkLock();
136+
137+
$this->validateSanitizeRelationshipWith = $validateSanitizeRelationshipWith;
138+
139+
return $this;
140+
}
141+
142+
/**
143+
* Get the callable to validate the relationship with.
144+
*
145+
* @since 2.0.0
146+
*
147+
* @return callable( mixed $thing ): ( Model | null )
148+
*/
149+
150+
public function getValidateSanitizeRelationshipWith(): callable {
151+
return $this->validateSanitizeRelationshipWith ?? static function( $thing ): ?Model {
152+
if ( null !== $thing && ! $thing instanceof Model ) {
153+
throw new InvalidArgumentException( 'Relationship value must be a valid value.' );
154+
}
155+
156+
return $thing;
157+
};
158+
}
159+
82160
/**
83161
* Set the relationship as belongs-to-many.
84162
*
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
namespace StellarWP\Models\Tests;
4+
5+
use StellarWP\Models\Model;
6+
use DateTime;
7+
8+
class BadMockModel extends Model {
9+
protected static array $properties = [
10+
'id' => 'int',
11+
'firstName' => [ 'string', 'Michael' ],
12+
'lastName' => 'string',
13+
'emails' => [ 'array', [] ],
14+
'microseconds' => 'float',
15+
'number' => 'int',
16+
'date' => DateTime::class,
17+
];
18+
}

tests/_support/Helper/MockModel.php

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
namespace StellarWP\Models\Tests;
44

55
use StellarWP\Models\Model;
6+
use StellarWP\Models\ModelPropertyDefinition;
7+
use DateTime;
68

79
class MockModel extends Model {
810
protected static array $properties = [
@@ -12,6 +14,15 @@ class MockModel extends Model {
1214
'emails' => [ 'array', [] ],
1315
'microseconds' => 'float',
1416
'number' => 'int',
15-
'date' => \DateTime::class,
1617
];
18+
19+
protected static function properties(): array {
20+
return [
21+
'date' => ( new ModelPropertyDefinition() )
22+
->type('object')
23+
->castWith(
24+
fn($value) => DateTime::createFromFormat('Y-m-d H:i:s', $value)
25+
),
26+
];
27+
}
1728
}

tests/_support/Helper/MockModelWithRelationship.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,13 @@ class MockModelWithRelationship extends Model {
2121
* @return ModelQueryBuilder<MockModel>
2222
*/
2323
public function relatedAndCallableHasOne(): ModelQueryBuilder {
24-
return ( new ModelQueryBuilder( MockModel::class ) )->from( 'posts' );
24+
return ( new ModelQueryBuilder( MockModel::class ) )->select( 'ID as id', 'post_title as firstName', 'post_content as lastName', 'post_status as emails', 'post_date as microseconds', 'post_date_gmt as number', 'post_date as date' )->from( 'posts' );
2525
}
2626

2727
/**
2828
* @return ModelQueryBuilder<MockModel>
2929
*/
3030
public function relatedAndCallableHasMany(): ModelQueryBuilder {
31-
return ( new ModelQueryBuilder( MockModel::class ) )->from( 'posts' );
31+
return ( new ModelQueryBuilder( MockModel::class ) )->select( 'ID as id', 'post_title as firstName', 'post_content as lastName', 'post_status as emails', 'post_date as microseconds', 'post_date_gmt as number', 'post_date as date' )->from( 'posts' );
3232
}
3333
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
*
2+
!.gitignore
3+
!.gitkeep

tests/_support/_generated/.gitkeep

Whitespace-only changes.

0 commit comments

Comments
 (0)