Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Too eager validation - fails common uses. #9

Closed
wants to merge 3 commits into from

Conversation

stratease
Copy link
Contributor

@stratease stratease commented Aug 25, 2023

This is to update our handling of type definitions in the Model. Some questions below.

@stratease stratease self-assigned this Aug 25, 2023
@@ -149,6 +149,7 @@ protected function getPropertyDefault( string $key ) {
protected function getPropertyDefaults() : array {
$defaults = [];
foreach ( array_keys( $this->properties ) as $property ) {
// @todo This could be an edge case bug, we shouldn't assume `null` is always a valid default value.
$defaults[ $property ] = $this->getPropertyDefault( $property );
Copy link
Contributor Author

@stratease stratease Aug 25, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An issue I can see happening here is a conflict between things that are explicitly being set to NULL versus ones that have not been set at all. This muddies that water, and makes it hard to know which situation is happening.

We ought to embrace MySQL and their DEFAULT and NULL features, and a PHP null would translate to a MySQL NULL. And a non-defined value (e.g. not set to NULL or any other value) should allow MySQL to leverage it's DEFAULT definitions.

And of course if we define a PHP default, that should take precedence and happen here, but I don't believe null in all other cases is safe.

https://dev.mysql.com/doc/refman/8.0/en/data-type-defaults.html

@@ -338,14 +377,13 @@ public static function propertyKeys() : array {
*/
public function setAttribute( string $key, $value ) : ModelInterface {
$this->validatePropertyExists( $key );
$this->validatePropertyType( $key, $value );
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Scenarios this would fail that we ought to support:

class Fish extends Model {
	protected $properties = [
		'eggs' => 'int',
	];
...

And in use...

// This would fatal.
$fish = new Fish();
$fish->eggs = '10';

Copy link
Contributor

@nikolaystrikhar nikolaystrikhar Apr 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While we don't want Fatal errors, it does not feel like deleting is a good solution. I wonder what @borkweb thinks about this approach.

@stratease stratease requested a review from borkweb August 25, 2023 23:40
@stratease stratease added enhancement New feature or request needs tests labels Aug 25, 2023
@stratease stratease marked this pull request as ready for review August 25, 2023 23:42
Comment on lines +358 to +359
case 'date':
return is_string( $value ) ? $value : $value->format( 'Y-m-d' );
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

			case 'date':
				return is_string( $value ) ? $value : $value->format( 'Y-m-d' );

Would like to start supporting date values. Thoughts?

@JasonTheAdams
Copy link
Contributor

JasonTheAdams commented Apr 19, 2024

I'll weigh in simply because this was originally taken from the GiveWP codebase, which I helped build. This doesn't mean this can't deviate from that, but I felt it worth explaining our rationale at the time.

We intentionally didn't want the models to work the way the Eloquent models do, wherein types are cast at the time of retrieval. The reason being we wanted to catch errors early in the process. Using the example you gave:

$fish = new Fish();
$fish->eggs = '10';

If this were a public int $eggs property with strict_types enabled, then yes, this would throw a type error. It raises the question: Why are we trying to assign a string to that property? What's happening at that point in time? The model, like a property, assumes that any prior validation/sanitization/casting has already occurred and it's now safe data. Consider this scenario:

$fish = new Fish();
$fish->eggs = 'buffalo';

That wouldn't be caught unless we now introduce some kind of is_numeric validation... but now we're validating in the model, too. Or we ignore it, in which case it's cast to a number and the problem is harder to follow.

This all comes down to the philosophy that errors should happen as soon as possible.

Hope this helps! 😄

@JasonTheAdams
Copy link
Contributor

As far as the nullable behavior is concerned, I wouldn't include that in this PR, as that's a separate concept than JIT casting. I can certainly see value in declaring whether a given property is nullable. 👍


$type = $this->getPropertyType( $key );

switch ( $type ) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe changing it with settype( $value, $type ); for multiple cases will be nicer, so you don't need to read all cases really as they are the same.

* @param string $key The property name.
* @param mixed $value The value to be cast.
*
* @return array|bool|int|string The $value after being cast to it's type.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the default case on line 363 we can return mixed still.

case 'string':
return (string) $value;
case 'bool':
return (bool) $value;
Copy link
Contributor

@nikolaystrikhar nikolaystrikhar Apr 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regarding booleans, there is a tricky moment in PHP. I'm not sure if we want to support it here, but we had such case in LearnDash, so want to make sure you are aware.

if ( 'bool' === $type ) {
    // We want to convert "false" string to false.
    $value = filter_var( $value, FILTER_VALIDATE_BOOLEAN );
}

… default value check, fixing a potential bug. WIP.
@stratease stratease requested a review from nikolaystrikhar May 3, 2024 22:08
@stratease
Copy link
Contributor Author

stratease commented May 3, 2024

I'll weigh in simply because this was originally taken from the GiveWP codebase, which I helped build. This doesn't mean this can't deviate from that, but I felt it worth explaining our rationale at the time.

We intentionally didn't want the models to work the way the Eloquent models do, wherein types are cast at the time of retrieval. The reason being we wanted to catch errors early in the process. Using the example you gave:

$fish = new Fish();
$fish->eggs = '10';

If this were a public int $eggs property with strict_types enabled, then yes, this would throw a type error. It raises the question: Why are we trying to assign a string to that property? What's happening at that point in time? The model, like a property, assumes that any prior validation/sanitization/casting has already occurred and it's now safe data. Consider this scenario:

$fish = new Fish();
$fish->eggs = 'buffalo';

That wouldn't be caught unless we now introduce some kind of is_numeric validation... but now we're validating in the model, too. Or we ignore it, in which case it's cast to a number and the problem is harder to follow.

This all comes down to the philosophy that errors should happen as soon as possible.

Hope this helps! 😄

Thanks, I don't disagree with that mentality at all. I prefer the explicit definitions. Although the side effect would be we need to have some more formal type casting into a DTO or other request input which we are missing at the moment. We need to avoid a lot of the $model->eggs = (int)$_GET['cnt'] mess that would come without that layer interacting with these models.

@stratease stratease closed this May 3, 2024
@JasonTheAdams
Copy link
Contributor

I think extending this to support object casting via interface would be awesome!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request needs tests
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants