Skip to content

Commit 68328c8

Browse files
committed
Merge remote-tracking branch 'origin/release/2.0.0' into feat/introduce-schema-model
2 parents 5c28c43 + 5011f49 commit 68328c8

File tree

8 files changed

+329
-32
lines changed

8 files changed

+329
-32
lines changed

README.md

Lines changed: 151 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ A library for a simple model structure.
88
* [Configuration](#configuration)
99
* [Creating a model](#creating-a-model)
1010
* [Interacting with a model](#interacting-with-a-model)
11+
* [Attribute validation](#attribute-validation)
1112
* [Data transfer objects](#data-transfer-objects)
1213
* [Classes of note](#classes-of-note)
1314
* [Model](#model)
@@ -57,7 +58,9 @@ Models are classes that hold data and provide some helper methods for interactin
5758

5859
### A simple model
5960

60-
This is an example of a model that just holds properties.
61+
This is an example of a model that just holds properties. Properties can be defined in one or both of the following ways:
62+
63+
#### Using the shorthand syntax:
6164

6265
```php
6366
namespace Boomshakalaka\Whatever;
@@ -70,14 +73,43 @@ class Breakfast_Model extends Model {
7073
*/
7174
protected static $properties = [
7275
'id' => 'int',
73-
'name' => 'string',
76+
'name' => ['string', 'Default Name'], // With default value
7477
'price' => 'float',
7578
'num_eggs' => 'int',
7679
'has_bacon' => 'bool',
7780
];
7881
}
7982
```
8083

84+
#### Using property definitions for more control:
85+
86+
```php
87+
namespace Boomshakalaka\Whatever;
88+
89+
use Boomshakalaka\StellarWP\Models\Model;
90+
use Boomshakalaka\StellarWP\Models\ModelPropertyDefinition;
91+
92+
class Breakfast_Model extends Model {
93+
/**
94+
* @inheritDoc
95+
*/
96+
protected static function properties(): array {
97+
return [
98+
'id' => ModelPropertyDefinition::create()
99+
->type('int')
100+
->required(),
101+
'name' => ModelPropertyDefinition::create()
102+
->type('string')
103+
->default('Default Name')
104+
->nullable(),
105+
'price' => ModelPropertyDefinition::create()
106+
->type('float')
107+
->requiredOnSave(),
108+
];
109+
}
110+
}
111+
```
112+
81113
### A ReadOnly model
82114

83115
This is a model whose intent is to only read and store data. The Read operations should - in most cases - be deferred to
@@ -183,6 +215,115 @@ class Breakfast_Model extends Model implements Contracts\ModelCrud {
183215
}
184216
```
185217

218+
## Interacting with a model
219+
220+
### Change tracking
221+
222+
Models track changes to their properties and provide methods to manage those changes:
223+
224+
```php
225+
$breakfast = new Breakfast_Model([
226+
'name' => 'Original Name',
227+
'price' => 5.99,
228+
]);
229+
230+
// Check if a property is dirty (changed)
231+
$breakfast->setAttribute('name', 'New Name');
232+
if ($breakfast->isDirty('name')) {
233+
echo 'Name has changed!';
234+
}
235+
236+
// Get all dirty values
237+
$dirtyValues = $breakfast->getDirty(); // ['name' => 'New Name']
238+
239+
// Commit changes (makes current values the "original")
240+
$breakfast->commitChanges();
241+
// or use the alias:
242+
$breakfast->syncOriginal();
243+
244+
// Revert a specific property change
245+
$breakfast->setAttribute('price', 7.99);
246+
$breakfast->revertChange('price'); // price is back to 5.99
247+
248+
// Revert all changes
249+
$breakfast->setAttribute('name', 'Another Name');
250+
$breakfast->setAttribute('price', 8.99);
251+
$breakfast->revertChanges(); // All properties back to original
252+
253+
// Get original value
254+
$originalName = $breakfast->getOriginal('name');
255+
$allOriginal = $breakfast->getOriginal(); // Get all original values
256+
```
257+
258+
### Checking if properties are set
259+
260+
The `isSet()` method checks if a property has been set. This is different from PHP's `isset()` because it considers `null` values and default values as "set":
261+
262+
```php
263+
$breakfast = new Breakfast_Model();
264+
265+
// Properties with defaults are considered set
266+
if ($breakfast->isSet('name')) { // true if 'name' has a default value
267+
echo 'Name is set';
268+
}
269+
270+
// Properties without defaults are not set until assigned
271+
if (!$breakfast->isSet('price')) { // false - no default and not assigned
272+
echo 'Price is not set';
273+
}
274+
275+
// Setting a property to null still counts as set
276+
$breakfast->setAttribute('price', null);
277+
if ($breakfast->isSet('price')) { // true - explicitly set to null
278+
echo 'Price is set (even though it\'s null)';
279+
}
280+
281+
// PHP's isset() behaves differently with null
282+
if (!isset($breakfast->price)) { // false - isset() returns false for null
283+
echo 'PHP isset() returns false for null values';
284+
}
285+
```
286+
287+
**Key differences from PHP's `isset()`:**
288+
- `isSet()` returns `true` for properties with default values
289+
- `isSet()` returns `true` for properties explicitly set to `null`
290+
- `isSet()` returns `false` only for properties that have no default and haven't been assigned
291+
292+
### Creating models from query data
293+
294+
Models can be created from database query results using the `fromData()` method:
295+
296+
```php
297+
// From an object or array
298+
$data = DB::get_row("SELECT * FROM breakfasts WHERE id = 1");
299+
$breakfast = Breakfast_Model::fromData($data);
300+
301+
// With different build modes
302+
$breakfast = Breakfast_Model::fromData($data, Breakfast_Model::BUILD_MODE_STRICT);
303+
$breakfast = Breakfast_Model::fromData($data, Breakfast_Model::BUILD_MODE_IGNORE_MISSING);
304+
$breakfast = Breakfast_Model::fromData($data, Breakfast_Model::BUILD_MODE_IGNORE_EXTRA);
305+
```
306+
307+
Build modes:
308+
- `BUILD_MODE_STRICT`: Throws exceptions for missing or extra properties
309+
- `BUILD_MODE_IGNORE_MISSING`: Ignores properties missing from the data
310+
- `BUILD_MODE_IGNORE_EXTRA`: Ignores extra properties in the data (default)
311+
312+
### Extending model construction
313+
314+
Models can perform custom initialization after construction by overriding the `afterConstruct()` method:
315+
316+
```php
317+
class Breakfast_Model extends Model {
318+
protected function afterConstruct() {
319+
// Perform custom initialization
320+
if ($this->has_bacon && $this->num_eggs > 2) {
321+
$this->setAttribute('name', $this->name . ' (Hearty!)');
322+
}
323+
}
324+
}
325+
```
326+
186327
## Attribute validation
187328

188329
Sometimes it would be helpful to validate attributes that are set in the model. To do that, you can create `validate_*()`
@@ -442,6 +583,13 @@ $breakfast = Breakfast_Model::find( 1 );
442583
$breakfast->delete();
443584
```
444585

586+
### Unsetting properties
587+
588+
```php
589+
$breakfast = Breakfast_Model::find( 1 );
590+
unset($breakfast->price); // Unsets the price property
591+
```
592+
445593
## Classes of note
446594

447595
### `Model`
@@ -455,7 +603,7 @@ This is an abstract class to extend for creating model factories.
455603
### `ModelQueryBuilder`
456604

457605
This class extends the [`stellarwp/db`](https://github.com/stellarwp/db) `QueryBuilder` class so that it returns
458-
model instances rather than arrays or `stdClass` instances. Using this requires models that implement the `ModelFromQueryBuilderObject`
606+
model instances rather than arrays or `stdClass` instances. Using this requires models that implement the `ModelBuildsFromData`
459607
interface.
460608

461609
### `DataTransferObject`

src/Models/Contracts/Model.php

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,26 @@ public function getDirty() : array;
5757
*
5858
* @return mixed|array
5959
*/
60-
public function getOriginal( string $key = null );
60+
public function getOriginal( ?string $key = null );
61+
62+
/**
63+
* Returns the property definition for the given key.
64+
*
65+
* @since 2.0.0
66+
*
67+
* @param string $key Property name.
68+
*
69+
* @return ModelPropertyDefinition
70+
*/
71+
public static function getPropertyDefinition( string $key ) : ModelPropertyDefinition;
72+
73+
/**
74+
* Returns the property definitions for the model.
75+
*
76+
* @since 2.0.0
77+
* @return array<string,ModelPropertyDefinition>
78+
*/
79+
public static function getPropertyDefinitions() : array;
6180

6281
/**
6382
* Returns the property definition for the given key.

src/Models/Model.php

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -86,10 +86,20 @@ abstract class Model implements ModelInterface, Arrayable, JsonSerializable {
8686
*
8787
* @param array<string,mixed> $attributes Attributes.
8888
*/
89-
public function __construct( array $attributes = [] ) {
89+
final public function __construct( array $attributes = [] ) {
9090
$this->propertyCollection = ModelPropertyCollection::fromPropertyDefinitions( static::getPropertyDefinitions(), $attributes );
91+
$this->afterConstruct();
9192
}
9293

94+
/**
95+
* This method is meant to be overridden by the model to perform actions after the model is constructed.
96+
*
97+
* @since 2.0.0
98+
*/
99+
protected function afterConstruct() {
100+
// This method is meant to be overridden by the model to perform actions after the model is constructed.
101+
return;
102+
}
93103
/**
94104
* Casts the value for the type, used when constructing a model from query data. If the model needs to support
95105
* additional types, especially class types, this method can be overridden.
@@ -101,8 +111,6 @@ public function __construct( array $attributes = [] ) {
101111
* @param string $property The property being casted.
102112
*
103113
* @return mixed
104-
*
105-
* @throws InvalidArgumentException If the value is not valid for the property.
106114
*/
107115
protected static function castValueForProperty( ModelPropertyDefinition $definition, $value, string $property ) {
108116
if ( $definition->isValidValue( $value ) || $value === null ) {
@@ -115,7 +123,7 @@ protected static function castValueForProperty( ModelPropertyDefinition $definit
115123

116124
$type = $definition->getType();
117125
if ( count( $type ) !== 1 ) {
118-
throw new InvalidArgumentException( "Property '$property' has multiple types: " . implode( ', ', $type ) . ". To support additional types, implement a custom castValueForProperty() method." );
126+
throw new \InvalidArgumentException( "Property '$property' has multiple types: " . implode( ', ', $type ) . ". To support additional types, implement a custom castValueForProperty() method." );
119127
}
120128

121129
switch ( $type[0] ) {
@@ -130,7 +138,7 @@ protected static function castValueForProperty( ModelPropertyDefinition $definit
130138
case 'float':
131139
return (float) filter_var( $value, FILTER_SANITIZE_NUMBER_FLOAT,FILTER_FLAG_ALLOW_FRACTION );
132140
default:
133-
Config::throwInvalidArgumentException( "Unexpected type: '$type'. To support additional types, overload this method or use Definition casting." );
141+
Config::throwInvalidArgumentException( "Unexpected type: '{$type[0]}'. To support additional types, overload this method or use Definition casting." );
134142
}
135143
}
136144

@@ -235,10 +243,10 @@ public static function getPropertyDefinition( string $key ): ModelPropertyDefini
235243
*
236244
* @throws InvalidArgumentException If the property key is not a string.
237245
*/
238-
public static function getPropertyDefinitions(): array {
239246
if ( isset( static::$cached_definitions[ static::class ] ) ) {
240247
return static::$cached_definitions[ static::class ];
241248
}
249+
public static function getPropertyDefinitions(): array {
242250

243251
$definitions = array_merge( static::$properties, static::properties() );
244252

@@ -448,6 +456,10 @@ public static function fromData($data, $mode = self::BUILD_MODE_IGNORE_EXTRA) {
448456

449457
foreach (static::$properties as $key => $type) {
450458
if ( ! array_key_exists( $key, $data ) ) {
459+
// Skip missing properties when BUILD_MODE_IGNORE_MISSING is set
460+
if ( $mode & self::BUILD_MODE_IGNORE_MISSING ) {
461+
continue;
462+
}
451463
Config::throwInvalidArgumentException( "Property '$key' does not exist." );
452464
}
453465

0 commit comments

Comments
 (0)