-
Notifications
You must be signed in to change notification settings - Fork 824
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
API Strong typing for the view layer #11351
Conversation
/** | ||
* 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]); | ||
} |
There was a problem hiding this comment.
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.
class ViewableData implements IteratorAggregate | ||
class ViewableData |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See #11351 (comment)
There was a problem hiding this 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.
fef4b86
to
0d02bcd
Compare
->setEmptyString($anyText); | ||
} | ||
|
||
public function nullValue() | ||
public function nullValue(): ?int |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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
* | ||
* @var array|DataObject | ||
*/ | ||
protected $record = []; | ||
protected array|ViewableData $record = []; |
There was a problem hiding this comment.
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.
return $this->config()->composite_db; | ||
return static::config()->get('composite_db'); |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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
.
public function __construct($name = null, $wholeSize = 9, $decimalSize = 2, $defaultValue = 0) | ||
{ | ||
parent::__construct($name, $wholeSize, $decimalSize, $defaultValue); | ||
} |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.
0d02bcd
to
3f21132
Compare
$this->defaultValue = number_format((float) $defaultValue, $decimalSize ?? 0); | ||
$this->defaultValue = number_format((float) $defaultValue, $this->decimalSize); |
There was a problem hiding this comment.
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
3f21132
to
bc9398d
Compare
* 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(); |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 = []; |
There was a problem hiding this comment.
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.
protected $object; | ||
protected ?DataObject $object; |
There was a problem hiding this comment.
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 ?? ''; |
There was a problem hiding this comment.
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)"; |
There was a problem hiding this comment.
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
.
src/View/SSTemplateParser.php
Outdated
There was a problem hiding this comment.
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
0268b65
to
22f0e48
Compare
public function testViewabledataItemsInsideArraydataArePreserved() | ||
public function testViewableDataItemsInsideArraydataArePreserved() |
There was a problem hiding this comment.
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.
22f0e48
to
3f69cb0
Compare
private function getResponseNegotiator(DBHTMLText $renderedForm): PjaxResponseNegotiator | ||
private function getResponseNegotiator(string $renderedForm): PjaxResponseNegotiator |
There was a problem hiding this comment.
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.
3f69cb0
to
b29ff22
Compare
{ | ||
return 0; | ||
} | ||
|
||
public function prepValueForDB($value) | ||
public function prepValueForDB(mixed $value): array|int|null |
There was a problem hiding this comment.
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.
8fbac57
to
bcc8d50
Compare
@@ -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(''); |
There was a problem hiding this comment.
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()
There was a problem hiding this comment.
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.
bcc8d50
to
01a133c
Compare
01a133c
to
3afd1b8
Compare
* @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 { |
There was a problem hiding this comment.
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.
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