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

API Strong typing for the view layer #11351

Merged
merged 1 commit into from
Aug 27, 2024

Conversation

GuySartorelli
Copy link
Member

I want a good base of known types for things that will be going into the template layer before I start abstracting it, and this gets me part of the way there.

Issue

Comment on lines -683 to -697
/**
* Return a single-item iterator so you can iterate over the fields of a single record.
*
* This is useful so you can use a single record inside a <% control %> block in a template - and then use
* to access individual fields on this object.
*
* @deprecated 5.2.0 Will be removed without equivalent functionality
*
* @return ArrayIterator
*/
public function getIterator(): Traversable
{
Deprecation::notice('5.2.0', 'Will be removed without equivalent functionality');
return new ArrayIterator([$this]);
}
Copy link
Member Author

Choose a reason for hiding this comment

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

Unrelated but seems sensible to just get this out of the way now - I'll need to know that things work as expected without this method there for the refactor anyway.

Comment on lines -37 to +31
class ViewableData implements IteratorAggregate
class ViewableData
Copy link
Member Author

Choose a reason for hiding this comment

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

Copy link
Member Author

@GuySartorelli GuySartorelli left a comment

Choose a reason for hiding this comment

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

All changes to use statements are unrelated refactoring, removing unused use statements.

@GuySartorelli GuySartorelli marked this pull request as draft August 22, 2024 21:38
->setEmptyString($anyText);
}

public function nullValue()
public function nullValue(): ?int
Copy link
Member Author

@GuySartorelli GuySartorelli Aug 22, 2024

Choose a reason for hiding this comment

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

Note that I have kept the following methods nullable for all DBField implementations intentionally:

  • getValue()
  • nullValue()
  • prepValueForDB()

This is so that subclasses can intentionally override these methods to return null. In the past I've had a business requirement for a nullable boolean and a nullable int (i.e. it's null until a content author explicitly selects an option rather than defaulting to 0 or false).

For that to work, these must be nullable types, even if in this implementation it's explicitly returning a non-null value.

*
* @var mixed
*/
protected $failover;
Copy link
Member Author

Choose a reason for hiding this comment

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

This property is already defined in ViewableData

@@ -521,7 +521,7 @@ public function setFieldMessage(
return $this;
}

public function castingHelper($field, bool $useFallback = true)
public function castingHelper(string $field, bool $useFallback = true): ?string
Copy link
Member Author

Choose a reason for hiding this comment

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

These are all nullable because ViewableData::castingHelper() returns null, and most if not all implementations of this method call parent::castingHelper()

*/
public function forTemplate()
public function forTemplate(): string
Copy link
Member Author

Choose a reason for hiding this comment

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

These should all return an explicit string. This is the raw HTML that will be directly input into the parent template.

*/
public function setField($fieldName, $val)
public function setField(string $fieldName, mixed $value): static
Copy link
Member Author

@GuySartorelli GuySartorelli Aug 22, 2024

Choose a reason for hiding this comment

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

Didn't really need to do setField but I was doing getField and hasField so it made sense to do this at the same time.

Changed parameter name is to be consistent with the parent class naming so that named arguments can be used more reliably. That will be a theme with some of these.


private static $index = true;
private static string|bool $index = true;
Copy link
Member Author

Choose a reason for hiding this comment

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

See the PHPDoc for this property in DBField for the reason behind this typing.

*/
public function setBaseClass($baseClass)
public function setBaseClass(?string $baseClass): static
Copy link
Member Author

Choose a reason for hiding this comment

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

Nullable param to allow resetting to the default value since the property itself is nullable

Comment on lines -46 to +45
*
* @var array|DataObject
*/
protected $record = [];
protected array|ViewableData $record = [];
Copy link
Member Author

Choose a reason for hiding this comment

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

Since this wasn't strongly typed before, I'm hesitant to jump straight to typing it to DataObject, since we don't really know how these are being used in the wild.
I'm cautiously typing against ViewableData instead. I've checked and everywhere that I've gone for ViewableData instead of DataObject is safe in terms of what methods are being called.

Comment on lines -117 to +107
return $this->config()->composite_db;
return static::config()->get('composite_db');
Copy link
Member Author

@GuySartorelli GuySartorelli Aug 22, 2024

Choose a reason for hiding this comment

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

Unrelated refactoring to align with best practices. There are a few other places where I've made similar changes.

*/
public function bindTo($dataObject)
public function bindTo(DataObject $dataObject): void
Copy link
Member Author

Choose a reason for hiding this comment

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

Unlike the places where I've gone for ViewableData, this one does seem to need to be DataObject explicitly. There's a conditional check elsewhere that only calls this method if the record is an instance of DataObject.

Comment on lines -28 to -31
public function __construct($name = null, $wholeSize = 9, $decimalSize = 2, $defaultValue = 0)
{
parent::__construct($name, $wholeSize, $decimalSize, $defaultValue);
}
Copy link
Member Author

Choose a reason for hiding this comment

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

This constructor is identical to the one in the parent class.

*/
public function setImmutable(bool $immutable): DBDatetime
protected static ?DBDatetime $mock_now = null;
Copy link
Member Author

Choose a reason for hiding this comment

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

Moved this - it used to be hidden below some of the methods.

Comment on lines -48 to +38
$this->defaultValue = number_format((float) $defaultValue, $decimalSize ?? 0);
$this->defaultValue = number_format((float) $defaultValue, $this->decimalSize);
Copy link
Member Author

Choose a reason for hiding this comment

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

Using 0 as a fallback was actually wrong - we should use the value we set for $this->decimalSize

Comment on lines -630 to 544
* Get formfield schema value
*
* @return string|array Encoded string for use in formschema response
* Get formfield schema value for use in formschema response
*/
public function getSchemaValue()
public function getSchemaValue(): mixed
{
return $this->RAW();
Copy link
Member Author

Choose a reason for hiding this comment

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

The raw value could be anything, so we can't constrain this type to string|array despite what the PHPDoc says.

{
$field = NumericField::create($this->name, $title);
$field->setScale(null); // remove no-decimal restriction
return $field;
}

public function nullValue()
public function nullValue(): ?int
Copy link
Member Author

Choose a reason for hiding this comment

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

Arguably should be float|int|null but realistically 0.0 and 0 are identical, and 0.0 is the only float that would make sense here.

* @var DataObject
*/
protected $object;
protected ?DataObject $object;
Copy link
Member Author

Choose a reason for hiding this comment

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

Explicitly DataObject rather than ViewableData here unlike other DBField implementations, because the foreignkey represents a relation to a DataObject.

@@ -70,11 +58,11 @@ public function scaffoldFormField($title = null, $params = null)
return $field;
}

public function setValue($value, $record = null, $markChanged = true)
public function setValue(mixed $value, null|array|ViewableData $record = null, bool $markChanged = true): static
Copy link
Member Author

@GuySartorelli GuySartorelli Aug 22, 2024

Choose a reason for hiding this comment

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

Keeping this as ViewableData mostly for consistency, but also because it's conceivable to save a reference to a DataObject into some arbitrary non-database-backed model (e.g. linking some db data up with some 3rd party integration).

/**
* List of html properties to whitelist
*/
protected array $whitelist = [];
Copy link
Member Author

Choose a reason for hiding this comment

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

Moved from below, because it was buried below some methods.

Comment on lines -16 to +15
protected $object;
protected ?DataObject $object;
Copy link
Member Author

@GuySartorelli GuySartorelli Aug 22, 2024

Choose a reason for hiding this comment

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

Just like with DBForeignKey, this is explicitly intended to represent the ID for a DataObject record so we're using DataObject instead of ViewableData here.

return $content;
return $content ?? '';
Copy link
Member Author

Choose a reason for hiding this comment

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

This was needed to respect the typehinting of methods that call this and return its value.
It's also just sensible.

$res['php'] .= "->$method('$property', null, true)";
$res['php'] .= "->$method('$property', [], true)";
Copy link
Member Author

Choose a reason for hiding this comment

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

This was needed to respect the typehint for obj() and XML_val() in ViewableData.

Copy link
Member Author

Choose a reason for hiding this comment

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

Todo comments here were automatically generated from the peg

@GuySartorelli GuySartorelli force-pushed the pulls/6/strong-typing branch 3 times, most recently from 0268b65 to 22f0e48 Compare August 22, 2024 23:51
public function testViewabledataItemsInsideArraydataArePreserved()
public function testViewableDataItemsInsideArraydataArePreserved()
Copy link
Member Author

Choose a reason for hiding this comment

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

unrelated refactor that happened as part of a broader search/replace that happened to fix a small typo.

private function getResponseNegotiator(DBHTMLText $renderedForm): PjaxResponseNegotiator
private function getResponseNegotiator(string $renderedForm): PjaxResponseNegotiator
Copy link
Member Author

@GuySartorelli GuySartorelli Aug 23, 2024

Choose a reason for hiding this comment

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

The DBHtmlText returned from rendering a template will be coerced to string when it's passed in here.
This change is necessary because we also pass the result of forTemplate() into this method, and that's explicitly a string.
We only need the string value in this method so it makes sense to type to that.

{
return 0;
}

public function prepValueForDB($value)
public function prepValueForDB(mixed $value): array|int|null
Copy link
Member Author

Choose a reason for hiding this comment

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

This method must always allow returning an array so that subclasses can do funky custom stuff. See MockDynamicAssignmentDBField in tests for an example of this.

@GuySartorelli GuySartorelli force-pushed the pulls/6/strong-typing branch 4 times, most recently from 8fbac57 to bcc8d50 Compare August 25, 2024 23:50
@@ -2369,7 +2372,7 @@ public function testPrimitivesConvertedToDBFields()
public function testMe(): void
{
$mockArrayData = $this->getMockBuilder(ArrayData::class)->addMethods(['forTemplate'])->getMock();
$mockArrayData->expects($this->once())->method('forTemplate');
$mockArrayData->expects($this->once())->method('forTemplate')->willReturn('');
Copy link
Member Author

Choose a reason for hiding this comment

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

Required to avoid returning null which breaks the type safety of ViewableData::XML_val()

Copy link
Member Author

Choose a reason for hiding this comment

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

Changes to this test are mostly because we're removing the getIterator from ViewableData - so we need to loop over an arraylist instead.

Comment on lines +496 to +506
* @return object|DBField|null The specific object representing the field, or null if there is no
* property, method, or dynamic data available for that field.
* Note that if there is a property or method that returns null, a relevant DBField instance will
* be returned.
*/
public function obj(
string $fieldName,
array $arguments = [],
bool $cache = false,
?string $cacheName = null
): ?object {
Copy link
Member Author

Choose a reason for hiding this comment

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

obj() can now return null.

This makes it incredibly clear the distinction between

  • we have a field or method, but it's returning null (will return an object), and
  • we have no field or method (will return null)

There were a few problems related to removing the iterator where you'd have <% loop $MissingProperty %> - and since there's no property it returned a DBText by default, and tried to loop over that.
Since we removed the iterator for ViewableData that started throwing an error. But it should neither try to iterate over it (there's nothing to iterate over) nor should it throw an error.

Now, because this method explicitly returns null, the template engine won't try to iterate over the missing value.

@GuySartorelli GuySartorelli marked this pull request as ready for review August 26, 2024 02:26
@emteknetnz emteknetnz merged commit ea93316 into silverstripe:6 Aug 27, 2024
5 of 13 checks passed
@emteknetnz emteknetnz deleted the pulls/6/strong-typing branch August 27, 2024 22:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants