Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
0bfba41
refactor: switches to static what can be
JasonTheAdams Mar 8, 2025
3bed92a
test: updates tests to use static properties/methods
JasonTheAdams Mar 8, 2025
4c05c26
refactor: switch more to static
JasonTheAdams Mar 8, 2025
24ba49b
test: updates remaining tests
JasonTheAdams Mar 8, 2025
b8e8831
feat: adds native model buildings from query data
JasonTheAdams Mar 8, 2025
6541267
chore: adds additional docs
JasonTheAdams Mar 8, 2025
2c2a2d4
chore: updates docs
JasonTheAdams Apr 9, 2025
0dcc15b
chore: updates outdated flows
JasonTheAdams Apr 9, 2025
dc52d77
Merge branch 'main' into refactor/static-methods-properties
JasonTheAdams Apr 9, 2025
7ff61d8
test: updates properties in tests
JasonTheAdams Apr 9, 2025
3bdedb7
chore: fixes syntax in readme
JasonTheAdams Apr 9, 2025
90b7a08
Merge branch 'refactor/static-methods-properties' into feature/from-q…
JasonTheAdams Apr 9, 2025
235d54e
fix: fetches relationship statically
JasonTheAdams Apr 9, 2025
cff40fb
Merge branch 'refactor/static-methods-properties' into feature/from-q…
JasonTheAdams Apr 9, 2025
99769e0
refactor: cleans up ModelQueryBuilder
JasonTheAdams Apr 9, 2025
254b408
fix: corrects static syntax
JasonTheAdams Apr 10, 2025
a7c7c3b
Merge branch 'refactor/static-methods-properties' into feature/from-q…
JasonTheAdams Apr 10, 2025
676dff6
feat: improves data casting
JasonTheAdams Apr 10, 2025
8a3bc18
refactor: corrects spacing
JasonTheAdams Apr 10, 2025
cf435ab
Merge branch 'release/2.0.0' into feature/from-query-data
JasonTheAdams Apr 12, 2025
8ec57b7
refactor: changes to fromData
JasonTheAdams Apr 12, 2025
dafd8ef
feat: skips casting if value is correct type
JasonTheAdams Apr 12, 2025
90a6def
fix: corrects changed interface
JasonTheAdams Apr 12, 2025
f433793
test: tries fixing syntax error
JasonTheAdams Apr 12, 2025
b99d67f
refactor: removes sttaic return type as it is not 7.4 compat
JasonTheAdams Apr 12, 2025
29359f3
chore: bumps min PHP version to 7.4
JasonTheAdams Apr 12, 2025
b838c79
test: adds fromData model test
JasonTheAdams Apr 12, 2025
77f50f2
test: adds fromModel exception test
JasonTheAdams Apr 12, 2025
8dd2f82
test: ignores extra data
JasonTheAdams Apr 12, 2025
7be8ba8
test: ignore missing
JasonTheAdams Apr 12, 2025
64bf370
test: marks tests as skipped for now
JasonTheAdams Apr 12, 2025
f69abd6
refactor: makes Model constructor final
JasonTheAdams Sep 26, 2025
eb571ca
refactor: updates to use fromData
JasonTheAdams Sep 26, 2025
d04c812
test: updates skipped tests
JasonTheAdams Sep 26, 2025
8e11e7b
refactor: adds explicit nullable types
JasonTheAdams Sep 26, 2025
18c9152
feat: adds afterConstruct overload method
JasonTheAdams Sep 29, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"config": {
"preferred-install": "dist",
"platform": {
"php": "7.0"
"php": "7.4"
},
"allow-plugins": {
"kylekatarnls/update-helper": true
Expand Down
11 changes: 10 additions & 1 deletion src/Models/Config.php
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,16 @@ public static function setInvalidArgumentException( string $class ) {
static::$invalidArgumentException = $class;
}

public static function throwInvalidArgumentException( string $message ) {
/**
* Convenience method for throwing the InvalidArgumentException.
*
* @since 2.0.0
*
* @param string $message
*
* @return void
*/
public static function throwInvalidArgumentException( string $message ): void {
throw new static::$invalidArgumentException( $message );
}
}
8 changes: 4 additions & 4 deletions src/Models/Contracts/Model.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

use RuntimeException;

interface Model {
interface Model extends ModelBuildsFromData {
/**
* Constructor.
*
Expand Down Expand Up @@ -56,7 +56,7 @@ public function getDirty() : array;
*
* @return mixed|array
*/
public function getOriginal( string $key = null );
public function getOriginal( ?string $key = null );

/**
* Determines if the model has the given property.
Expand All @@ -79,7 +79,7 @@ public static function hasProperty( string $key ) : bool;
*
* @return bool
*/
public function isClean( string $attribute = null ) : bool;
public function isClean( ?string $attribute = null ) : bool;

/**
* Determines if a given attribute is dirty.
Expand All @@ -90,7 +90,7 @@ public function isClean( string $attribute = null ) : bool;
*
* @return bool
*/
public function isDirty( string $attribute = null ) : bool;
public function isDirty( ?string $attribute = null ) : bool;

/**
* Validates an attribute to a PHP type.
Expand Down
19 changes: 19 additions & 0 deletions src/Models/Contracts/ModelBuildsFromData.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

namespace StellarWP\Models\Contracts;

use StellarWP\Models\ModelQueryBuilder;

/**
* @since 2.0.0
*/
interface ModelBuildsFromData {
/**
* @since 2.0.0
*
* @param array|object $data
*
* @return Model
*/
public static function fromData( $data );
}
19 changes: 0 additions & 19 deletions src/Models/Contracts/ModelFromQueryBuilderObject.php

This file was deleted.

11 changes: 1 addition & 10 deletions src/Models/Contracts/ModelReadOnly.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
/**
* @since 1.0.0
*/
interface ModelReadOnly {
interface ModelReadOnly extends ModelBuildsFromQueryData {
/**
* @since 1.0.0
*
Expand All @@ -23,13 +23,4 @@ public static function find( $id );
* @return ModelQueryBuilder
*/
public static function query();

/**
* @since 1.0.0
*
* @param $object
*
* @return Model
*/
public static function fromQueryBuilderObject( $object );
}
100 changes: 96 additions & 4 deletions src/Models/Model.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@
use StellarWP\Models\ValueObjects\Relationship;

abstract class Model implements ModelInterface, Arrayable, JsonSerializable {
public const BUILD_MODE_STRICT = 0;
public const BUILD_MODE_IGNORE_MISSING = 1;
public const BUILD_MODE_IGNORE_EXTRA = 2;

/**
* The model's attributes.
*
Expand Down Expand Up @@ -51,10 +55,55 @@ abstract class Model implements ModelInterface, Arrayable, JsonSerializable {
*
* @param array<string,mixed> $attributes Attributes.
*/
public function __construct( array $attributes = [] ) {
final public function __construct( array $attributes = [] ) {
$this->fill( array_merge( static::getPropertyDefaults(), $attributes ) );

$this->syncOriginal();

$this->afterConstruct();
}

/**
* This method is meant to be overridden by the model to perform actions after the model is constructed.
*
* @since 2.0.0
*/
protected function afterConstruct() {
// This method is meant to be overridden by the model to perform actions after the model is constructed.
return;
}

/**
* Casts the value for the type, used when constructing a model from query data. If the model needs to support
* additional types, especially class types, this method can be overridden.
*
* @since 2.0.0 changed to static
*
* @param string $type
* @param mixed $value The query data value to cast, probably a string.
* @param string $property The property being casted.
*
* @return mixed
*/
protected static function castValueForProperty( string $type, $value, string $property ) {
if ( static::isPropertyTypeValid( $property, $value ) || $value === null ) {
return $value;
}

switch ( $type ) {
case 'int':
return (int) $value;
case 'string':
return (string) $value;
case 'bool':
return (bool) filter_var( $value, FILTER_VALIDATE_BOOLEAN );
case 'array':
return (array) $value;
case 'float':
return (float) filter_var( $value, FILTER_SANITIZE_NUMBER_FLOAT,FILTER_FLAG_ALLOW_FRACTION );
default:
Config::throwInvalidArgumentException( "Unexpected type: '$type'. To support additional types, implement a custom castValueForProperty() method." );
}
}

/**
Expand Down Expand Up @@ -120,7 +169,7 @@ public function getDirty() : array {
*
* @return mixed|array
*/
public function getOriginal( string $key = null ) {
public function getOriginal( ?string $key = null ) {
return $key ? $this->original[ $key ] : $this->original;
}

Expand Down Expand Up @@ -285,7 +334,7 @@ public static function hasProperty( string $key ) : bool {
*
* @return bool
*/
public function isClean( string $attribute = null ) : bool {
public function isClean( ?string $attribute = null ) : bool {
return ! $this->isDirty( $attribute );
}

Expand All @@ -298,7 +347,7 @@ public function isClean( string $attribute = null ) : bool {
*
* @return bool
*/
public function isDirty( string $attribute = null ) : bool {
public function isDirty( ?string $attribute = null ) : bool {
if ( ! $attribute ) {
return (bool) $this->getDirty();
}
Expand Down Expand Up @@ -354,6 +403,49 @@ public function jsonSerialize() {
return get_object_vars( $this );
}

/**
* Constructs a model instance from database query data.
*
* @param object|array $queryData
* @param int $mode The level of strictness to take when constructing the object, by default it will ignore extra keys but error on missing keys.
* @return static
*/
public static function fromData($data, $mode = self::BUILD_MODE_IGNORE_EXTRA) {
if ( ! is_object( $data ) && ! is_array( $data ) ) {
Config::throwInvalidArgumentException( 'Query data must be an object or array' );
}

$data = (array) $data;

// If we're not ignoring extra keys, check for them and throw an exception if any are found.
if ( ! ($mode & self::BUILD_MODE_IGNORE_EXTRA) ) {
$extraKeys = array_diff_key( (array) $data, static::$properties );
if ( ! empty( $extraKeys ) ) {
Config::throwInvalidArgumentException( 'Query data contains extra keys: ' . implode( ', ', array_keys( $extraKeys ) ) );
}
}

if ( ! ($mode & self::BUILD_MODE_IGNORE_MISSING) ) {
$missingKeys = array_diff_key( static::$properties, (array) $data );
if ( ! empty( $missingKeys ) ) {
Config::throwInvalidArgumentException( 'Query data is missing keys: ' . implode( ', ', array_keys( $missingKeys ) ) );
}
}

$instance = new static();

foreach (static::$properties as $key => $type) {
if ( ! array_key_exists( $key, $data ) ) {
Config::throwInvalidArgumentException( "Property '$key' does not exist." );
}

// Remember not to use $type, as it may be an array that includes the default value. Safer to use getPropertyType().
$instance->setAttribute($key, static::castValueForProperty(static::getPropertyType($key), $data[$key], $key));
}

return $instance;
}

/**
* Returns the property keys.
*
Expand Down
79 changes: 22 additions & 57 deletions src/Models/ModelQueryBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,17 @@
use StellarWP\DB\DB;
use StellarWP\DB\QueryBuilder\QueryBuilder;
use StellarWP\DB\QueryBuilder\Clauses\RawSQL;
use StellarWP\Models\Model;

use StellarWP\Models\Contracts\Model;
use StellarWP\Models\Contracts\ModelBuildsFromData;
/**
* @since 1.2.2 improve model generic
* @since 1.0.0
*
* @template M of Model
* @template M of ModelBuildsFromData
*/
class ModelQueryBuilder extends QueryBuilder {
public const MODEL = 'model';

/**
* @var class-string<M>
*/
Expand All @@ -24,8 +26,8 @@ class ModelQueryBuilder extends QueryBuilder {
* @param class-string<M> $modelClass
*/
public function __construct( string $modelClass ) {
if ( ! is_subclass_of( $modelClass, Model::class ) ) {
throw new InvalidArgumentException( "$modelClass must be an instance of " . Model::class );
if ( ! is_subclass_of( $modelClass, ModelBuildsFromData::class ) ) {
throw new InvalidArgumentException( "$modelClass must implement " . ModelBuildsFromData::class );
}

$this->model = $modelClass;
Expand All @@ -38,7 +40,7 @@ public function __construct( string $modelClass ) {
*
* @param null|string $column
*/
public function count( $column = null ) : int {
public function count( ?string $column = null ) : int {
$column = ( ! $column || $column === '*' ) ? '1' : trim( $column );

if ( '1' === $column ) {
Expand All @@ -58,14 +60,18 @@ public function count( $column = null ) : int {
*
* @return M|null
*/
public function get( $output = OBJECT ): ?Model {
$row = DB::get_row( $this->getSQL(), OBJECT );
public function get( $output = self::MODEL ): ?Model {
if ( $output !== self::MODEL ) {
return parent::get( $output );
}

$row = DB::get_row( $this->getSQL() );

if ( ! $row ) {
return null;
}

return $this->getRowAsModel( $row );
return $this->model::fromData( $row );
}

/**
Expand All @@ -75,58 +81,17 @@ public function get( $output = OBJECT ): ?Model {
*
* @return M[]|null
*/
public function getAll( $output = OBJECT ) : ?array {
$results = DB::get_results( $this->getSQL(), OBJECT );

if ( ! $results ) {
return null;
}

if ( isset( $this->model ) ) {
return $this->getAllAsModel( $results );
}

return $results;
}

/**
* Get row as model
*
* @since 1.0.0
*
* @param object|null $row
*
* @return M|null
*/
protected function getRowAsModel( $row ) {
$model = $this->model;

if ( ! method_exists( $model, 'fromQueryBuilderObject' ) ) {
throw new InvalidArgumentException( "fromQueryBuilderObject missing from $model" );
public function getAll( $output = self::MODEL ) : ?array {
if ( $output !== self::MODEL ) {
return parent::getAll( $output );
}

return $model::fromQueryBuilderObject( $row );
}

/**
* Get results as models
*
* @since 1.0.0
*
* @param object[] $results
*
* @return M[]|null
*/
protected function getAllAsModel( array $results ) {
/** @var Contracts\ModelCrud $model */
$model = $this->model;
$results = DB::get_results( $this->getSQL() );

if ( ! method_exists( $model, 'fromQueryBuilderObject' ) ) {
throw new InvalidArgumentException( "fromQueryBuilderObject missing from $model" );
if ( ! $results ) {
return null;
}

return array_map( static function( $object ) use ( $model ) {
return $model::fromQueryBuilderObject( $object );
}, $results );
return array_map( [ $this->model, 'fromData' ], $results );
}
}
Loading