diff --git a/composer.json b/composer.json index 7f33acac9..dd2d9504b 100644 --- a/composer.json +++ b/composer.json @@ -27,7 +27,7 @@ "composer/installers": "^2.2", "guzzlehttp/guzzle": "^7.5.0", "guzzlehttp/psr7": "^2.4.0", - "embed/embed": "^4.4.4", + "embed/embed": "^4.4.7", "league/csv": "^9.8.0", "m1/env": "^2.2.0", "monolog/monolog": "^3.2.0", diff --git a/src/Control/HTTPRequest.php b/src/Control/HTTPRequest.php index 2ceb7e239..db22f8d41 100644 --- a/src/Control/HTTPRequest.php +++ b/src/Control/HTTPRequest.php @@ -579,7 +579,7 @@ class HTTPRequest implements ArrayAccess $shiftCount = sizeof($patternParts ?? []); $remaining = count($this->dirParts ?? []) - $i; for ($j = 1; $j <= $remaining; $j++) { - $arguments["$${j}"] = $this->dirParts[$j + $i - 1]; + $arguments['$' . $j] = $this->dirParts[$j + $i - 1]; } $patternParts = array_merge($patternParts, array_keys($arguments ?? [])); break; diff --git a/src/Dev/Constraint/SSListContains.php b/src/Dev/Constraint/SSListContains.php index bb3b0b9dd..e9880ee22 100644 --- a/src/Dev/Constraint/SSListContains.php +++ b/src/Dev/Constraint/SSListContains.php @@ -20,6 +20,8 @@ class SSListContains extends Constraint implements TestOnly */ protected $matches = []; + protected SSListExporter $exporter; + /** * Check if the list has left over items that don't match * diff --git a/src/Dev/Constraint/SSListContainsOnlyMatchingItems.php b/src/Dev/Constraint/SSListContainsOnlyMatchingItems.php index f093b3e01..a08b20f53 100644 --- a/src/Dev/Constraint/SSListContainsOnlyMatchingItems.php +++ b/src/Dev/Constraint/SSListContainsOnlyMatchingItems.php @@ -19,6 +19,8 @@ class SSListContainsOnlyMatchingItems extends Constraint implements TestOnly */ private $match; + protected SSListExporter $exporter; + /** * @var ViewableDataContains */ diff --git a/src/Dev/FixtureBlueprint.php b/src/Dev/FixtureBlueprint.php index e0c79bec6..7652f6ddb 100644 --- a/src/Dev/FixtureBlueprint.php +++ b/src/Dev/FixtureBlueprint.php @@ -34,6 +34,8 @@ class FixtureBlueprint */ protected $class; + private FixtureFactory $factory; + /** * @var array */ @@ -70,6 +72,17 @@ class FixtureBlueprint $this->defaults = $defaults; } + public function getFactory(): FixtureFactory + { + return $this->factory; + } + + public function setFactory(FixtureFactory $factory): static + { + $this->factory = $factory; + return $this; + } + /** * @param string $identifier Unique identifier for this fixture type * @param array $data Map of property names to their values. diff --git a/src/Dev/SapphireTest.php b/src/Dev/SapphireTest.php index a31675502..65c63dab5 100644 --- a/src/Dev/SapphireTest.php +++ b/src/Dev/SapphireTest.php @@ -161,6 +161,8 @@ abstract class SapphireTest extends TestCase implements TestOnly */ protected static $tempDB = null; + protected FixtureFactory|bool $fixtureFactory; + /** * @return TempDatabase */ diff --git a/src/ORM/Connect/MySQLDatabase.php b/src/ORM/Connect/MySQLDatabase.php index ef7cd1601..b80e02054 100644 --- a/src/ORM/Connect/MySQLDatabase.php +++ b/src/ORM/Connect/MySQLDatabase.php @@ -65,6 +65,8 @@ class MySQLDatabase extends Database implements TransactionManager */ private $transactionManager = null; + private int $transactionNesting = 0; + /** * Default collation * diff --git a/src/ORM/Connect/MySQLStatement.php b/src/ORM/Connect/MySQLStatement.php index da6878988..81edf8c50 100644 --- a/src/ORM/Connect/MySQLStatement.php +++ b/src/ORM/Connect/MySQLStatement.php @@ -73,7 +73,6 @@ class MySQLStatement extends Query public function __destruct() { $this->statement->close(); - $this->currentRecord = false; } /** diff --git a/src/ORM/DataObject.php b/src/ORM/DataObject.php index f66dc252a..b2d4ad017 100644 --- a/src/ORM/DataObject.php +++ b/src/ORM/DataObject.php @@ -6,6 +6,7 @@ use BadMethodCallException; use Exception; use InvalidArgumentException; use LogicException; +use SilverStripe\Assets\Storage\DBFile; use SilverStripe\Core\ClassInfo; use SilverStripe\Core\Config\Config; use SilverStripe\Core\Injector\Injector; @@ -2841,51 +2842,57 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity // later referenced to update the parent dataobject if ($val instanceof DBComposite) { $val->bindTo($this); - $this->record[$fieldName] = $val; + $this->setFieldValue($fieldName, $val); } // Situation 2: Passing a literal or non-DBField object } else { - // If this is a proper database field, we shouldn't be getting non-DBField objects - if (is_object($val) && $schema->fieldSpec(static::class, $fieldName)) { - throw new InvalidArgumentException('DataObject::setField: passed an object that is not a DBField'); - } - - if (!empty($val) && !is_scalar($val)) { - $dbField = $this->dbObject($fieldName); - if ($dbField && $dbField->scalarValueOnly()) { - throw new InvalidArgumentException( - sprintf( - 'DataObject::setField: %s only accepts scalars', - $fieldName - ) - ); - } - } - - // if a field is not existing or has strictly changed - if (!array_key_exists($fieldName, $this->original ?? []) || $this->original[$fieldName] !== $val) { - // TODO Add check for php-level defaults which are not set in the db - // TODO Add check for hidden input-fields (readonly) which are not set in the db - // At the very least, the type has changed - $this->changed[$fieldName] = self::CHANGE_STRICT; - - if ((!array_key_exists($fieldName, $this->original ?? []) && $val) - || (array_key_exists($fieldName, $this->original ?? []) && $this->original[$fieldName] != $val) - ) { - // Value has changed as well, not just the type - $this->changed[$fieldName] = self::CHANGE_VALUE; - } - // Value has been restored to its original, remove any record of the change - } elseif (isset($this->changed[$fieldName])) { - unset($this->changed[$fieldName]); - } - - // Value is saved regardless, since the change detection relates to the last write - $this->record[$fieldName] = $val; + $this->setFieldValue($fieldName, $val); } return $this; } + private function setFieldValue(string $fieldName, mixed $val): void + { + $schema = static::getSchema(); + // If this is a proper database field, we shouldn't be getting non-DBField objects + if (is_object($val) && !($val instanceof DBField) && $schema->fieldSpec(static::class, $fieldName)) { + throw new InvalidArgumentException('DataObject::setFieldValue: passed an object that is not a DBField'); + } + + if (!empty($val) && !is_scalar($val)) { + $dbField = $this->dbObject($fieldName); + if ($dbField && $dbField->scalarValueOnly()) { + throw new InvalidArgumentException( + sprintf( + 'DataObject::setFieldValue: %s only accepts scalars', + $fieldName + ) + ); + } + } + + // if a field is not existing or has strictly changed + if (!array_key_exists($fieldName, $this->original ?? []) || $this->original[$fieldName] !== $val) { + // TODO Add check for php-level defaults which are not set in the db + // TODO Add check for hidden input-fields (readonly) which are not set in the db + // At the very least, the type has changed + $this->changed[$fieldName] = self::CHANGE_STRICT; + + if ((!array_key_exists($fieldName, $this->original ?? []) && $val) + || (array_key_exists($fieldName, $this->original ?? []) && $this->original[$fieldName] != $val) + ) { + // Value has changed as well, not just the type + $this->changed[$fieldName] = self::CHANGE_VALUE; + } + // Value has been restored to its original, remove any record of the change + } elseif (isset($this->changed[$fieldName])) { + unset($this->changed[$fieldName]); + } + + // Value is saved regardless, since the change detection relates to the last write + $this->record[$fieldName] = $val; + } + /** * Set the value of the field, using a casting object. * This is useful when you aren't sure that a date is in SQL format, for example. diff --git a/src/ORM/FieldType/DBBoolean.php b/src/ORM/FieldType/DBBoolean.php index f96c40bf6..a0b485f5d 100644 --- a/src/ORM/FieldType/DBBoolean.php +++ b/src/ORM/FieldType/DBBoolean.php @@ -46,7 +46,7 @@ class DBBoolean extends DBField { $fieldName = $this->name; if ($fieldName) { - $dataObject->$fieldName = ($this->value) ? 1 : 0; + $dataObject->setField($fieldName, $this->value ? 1 : 0); } else { $class = static::class; throw new \RuntimeException("DBField::saveInto() Called on a nameless '$class' object"); diff --git a/src/ORM/FieldType/DBDecimal.php b/src/ORM/FieldType/DBDecimal.php index 5af228bf8..0482f35f0 100644 --- a/src/ORM/FieldType/DBDecimal.php +++ b/src/ORM/FieldType/DBDecimal.php @@ -88,7 +88,8 @@ class DBDecimal extends DBField $fieldName = $this->name; if ($fieldName) { - $dataObject->$fieldName = (float)preg_replace('/[^0-9.\-\+]/', '', $this->value ?? ''); + $value = (float) preg_replace('/[^0-9.\-\+]/', '', $this->value ?? ''); + $dataObject->setField($fieldName, $value); } else { throw new \UnexpectedValueException( "DBField::saveInto() Called on a nameless '" . static::class . "' object" diff --git a/src/ORM/FieldType/DBField.php b/src/ORM/FieldType/DBField.php index 471ddc614..0c96442fc 100644 --- a/src/ORM/FieldType/DBField.php +++ b/src/ORM/FieldType/DBField.php @@ -542,7 +542,7 @@ abstract class DBField extends ViewableData implements DBIndexable "DBField::saveInto() Called on a nameless '" . static::class . "' object" ); } - $dataObject->$fieldName = $this->value; + $dataObject->setField($fieldName, $this->value); } /** diff --git a/src/ORM/FieldType/DBPercentage.php b/src/ORM/FieldType/DBPercentage.php index 56749ff76..dca55431c 100644 --- a/src/ORM/FieldType/DBPercentage.php +++ b/src/ORM/FieldType/DBPercentage.php @@ -45,7 +45,7 @@ class DBPercentage extends DBDecimal $fieldName = $this->name; if ($fieldName && $dataObject->$fieldName > 1.0) { - $dataObject->$fieldName = 1.0; + $dataObject->setField($fieldName, 1.0); } } } diff --git a/src/View/ViewableData.php b/src/View/ViewableData.php index 2440a5cac..9731747ee 100644 --- a/src/View/ViewableData.php +++ b/src/View/ViewableData.php @@ -66,6 +66,11 @@ class ViewableData implements IteratorAggregate */ private static $casting_cache = []; + /** + * Acts as a PHP 8.2+ compliant replacement for dynamic properties + */ + private array $data = []; + // ----------------------------------------------------------------------------------------------------------------- /** @@ -191,7 +196,7 @@ class ViewableData implements IteratorAggregate */ public function hasField($field) { - return property_exists($this, $field ?? ''); + return property_exists($this, $field) || isset($this->data[$field]); } /** @@ -202,7 +207,10 @@ class ViewableData implements IteratorAggregate */ public function getField($field) { - return $this->$field; + if (property_exists($this, $field)) { + return $this->$field; + } + return $this->data[$field]; } /** @@ -215,7 +223,13 @@ class ViewableData implements IteratorAggregate public function setField($field, $value) { $this->objCacheClear(); - $this->$field = $value; + // prior to PHP 8.2 support ViewableData::setField() simply used `$this->field = $value;` + // so the following logic essentially mimics this behaviour, though without the use + // of now deprecated dynamic properties + if (property_exists($this, $field)) { + $this->$field = $value; + } + $this->data[$field] = $value; return $this; } @@ -509,14 +523,14 @@ class ViewableData implements IteratorAggregate * A simple wrapper around {@link ViewableData::obj()} that automatically caches the result so it can be used again * without re-running the method. * - * @param string $field + * @param string $fieldName * @param array $arguments * @param string $identifier an optional custom cache identifier * @return Object|DBField */ - public function cachedCall($field, $arguments = [], $identifier = null) + public function cachedCall($fieldName, $arguments = [], $identifier = null) { - return $this->obj($field, $arguments, true, $identifier); + return $this->obj($fieldName, $arguments, true, $identifier); } /** diff --git a/src/View/ViewableData_Customised.php b/src/View/ViewableData_Customised.php index 097da4781..a8589bb51 100644 --- a/src/View/ViewableData_Customised.php +++ b/src/View/ViewableData_Customised.php @@ -59,19 +59,26 @@ class ViewableData_Customised extends ViewableData return $this->customised->hasMethod($method) || $this->original->hasMethod($method); } - public function cachedCall($field, $arguments = null, $identifier = null) + public function cachedCall($fieldName, $arguments = null, $identifier = null) { - if ($this->customised->hasMethod($field) || $this->customised->hasField($field)) { - return $this->customised->cachedCall($field, $arguments, $identifier); + if ($this->customisedHas($fieldName)) { + return $this->customised->cachedCall($fieldName, $arguments, $identifier); } - return $this->original->cachedCall($field, $arguments, $identifier); + return $this->original->cachedCall($fieldName, $arguments, $identifier); } public function obj($fieldName, $arguments = null, $cache = false, $cacheName = null) { - if ($this->customised->hasField($fieldName) || $this->customised->hasMethod($fieldName)) { + if ($this->customisedHas($fieldName)) { return $this->customised->obj($fieldName, $arguments, $cache, $cacheName); } return $this->original->obj($fieldName, $arguments, $cache, $cacheName); } + + private function customisedHas(string $fieldName): bool + { + return property_exists($this->customised, $fieldName) || + $this->customised->hasField($fieldName) || + $this->customised->hasMethod($fieldName); + } } diff --git a/tests/php/Core/Injector/AopProxyServiceTest/AnotherService.php b/tests/php/Core/Injector/AopProxyServiceTest/AnotherService.php index b91ecb8b4..cf4d574cc 100644 --- a/tests/php/Core/Injector/AopProxyServiceTest/AnotherService.php +++ b/tests/php/Core/Injector/AopProxyServiceTest/AnotherService.php @@ -4,5 +4,7 @@ namespace SilverStripe\Core\Tests\Injector\AopProxyServiceTest; class AnotherService { + public $config_property; + public $filters = []; } diff --git a/tests/php/Core/Injector/AopProxyServiceTest/SampleService.php b/tests/php/Core/Injector/AopProxyServiceTest/SampleService.php index 7190bdf76..af17e0492 100644 --- a/tests/php/Core/Injector/AopProxyServiceTest/SampleService.php +++ b/tests/php/Core/Injector/AopProxyServiceTest/SampleService.php @@ -4,6 +4,7 @@ namespace SilverStripe\Core\Tests\Injector\AopProxyServiceTest; class SampleService { + public $auto; public $constructorVarOne; public $constructorVarTwo; diff --git a/tests/php/Core/Injector/InjectorTest/TestObject.php b/tests/php/Core/Injector/InjectorTest/TestObject.php index 075dcee02..01aa7d6c2 100644 --- a/tests/php/Core/Injector/InjectorTest/TestObject.php +++ b/tests/php/Core/Injector/InjectorTest/TestObject.php @@ -6,6 +6,7 @@ use SilverStripe\Dev\TestOnly; class TestObject implements TestOnly { + public $auto; public $sampleService; diff --git a/tests/php/Dev/FixtureBlueprintTest.php b/tests/php/Dev/FixtureBlueprintTest.php index 6224d5783..b8fa977fa 100644 --- a/tests/php/Dev/FixtureBlueprintTest.php +++ b/tests/php/Dev/FixtureBlueprintTest.php @@ -17,6 +17,8 @@ class FixtureBlueprintTest extends SapphireTest protected $usesDatabase = true; + private int $_called = 0; + protected static $extra_dataobjects = [ TestDataObject::class, DataObjectRelation::class, diff --git a/tests/php/Forms/GridField/GridFieldFilterHeaderTest.php b/tests/php/Forms/GridField/GridFieldFilterHeaderTest.php index 357c9d765..1081f90f3 100644 --- a/tests/php/Forms/GridField/GridFieldFilterHeaderTest.php +++ b/tests/php/Forms/GridField/GridFieldFilterHeaderTest.php @@ -138,18 +138,17 @@ class GridFieldFilterHeaderTest extends SapphireTest public function testGetSearchForm() { $searchForm = $this->component->getSearchForm($this->gridField); - $this->assertTrue($searchForm instanceof Form); - $this->assertEquals('Search__q', $searchForm->fields[0]->Name); - $this->assertEquals('Search__Name', $searchForm->fields[1]->Name); - $this->assertEquals('Search__City', $searchForm->fields[2]->Name); - $this->assertEquals('Search__Cheerleader__Hat__Colour', $searchForm->fields[3]->Name); + $fields = $searchForm->Fields()->toArray(); + $this->assertEquals('Search__q', $fields[0]->Name); + $this->assertEquals('Search__Name', $fields[1]->Name); + $this->assertEquals('Search__City', $fields[2]->Name); + $this->assertEquals('Search__Cheerleader__Hat__Colour', $fields[3]->Name); $this->assertEquals('TeamsSearchForm', $searchForm->Name); - $this->assertEquals('cms-search-form', $searchForm->extraClasses['cms-search-form']); - - foreach ($searchForm->fields as $field) { - $this->assertEquals('stacked', $field->extraClasses['stacked']); - $this->assertEquals('no-change-track', $field->extraClasses['no-change-track']); + $this->assertTrue($searchForm->hasExtraClass('cms-search-form')); + foreach ($fields as $field) { + $this->assertTrue($field->hasExtraClass('stacked')); + $this->assertTrue($field->hasExtraClass('no-change-track')); } } diff --git a/tests/php/ORM/DataObjectTest.php b/tests/php/ORM/DataObjectTest.php index cb6536f57..e4617bb7c 100644 --- a/tests/php/ORM/DataObjectTest.php +++ b/tests/php/ORM/DataObjectTest.php @@ -826,10 +826,6 @@ class DataObjectTest extends SapphireTest $team1->Captain = $captain2; $team1->write(); $this->assertEquals($captain2->ID, $team1->Captain->ID); - - // Setter: Custom data (required by DataDifferencer) - $team1->Captain = DBField::create_field('HTMLFragment', '
No captain
'); - $this->assertEquals('No captain
', $team1->Captain); } /** diff --git a/tests/php/View/ArrayDataTest/NonEmptyObject.php b/tests/php/View/ArrayDataTest/NonEmptyObject.php index fa25d7c5c..da4cc57e2 100644 --- a/tests/php/View/ArrayDataTest/NonEmptyObject.php +++ b/tests/php/View/ArrayDataTest/NonEmptyObject.php @@ -6,8 +6,9 @@ use SilverStripe\Dev\TestOnly; class NonEmptyObject implements TestOnly { - - static $c = "Cucumber"; + public $a; + public $b; + public static $c = "Cucumber"; public function __construct() { diff --git a/tests/php/View/Embed/MockResponse.php b/tests/php/View/Embed/MockResponse.php index 2c71e179b..8744e49c7 100644 --- a/tests/php/View/Embed/MockResponse.php +++ b/tests/php/View/Embed/MockResponse.php @@ -8,7 +8,7 @@ use Psr\Http\Message\StreamInterface; class MockResponse implements ResponseInterface { private EmbedUnitTest $unitTest; - private string $firstReponse; + private string $firstResponse; private string $secondResponse; public function __construct(EmbedUnitTest $unitTest, string $firstResponse, string $secondResponse) diff --git a/thirdparty/php-peg/Parser.php b/thirdparty/php-peg/Parser.php index 1ce89193e..bcb57ce06 100644 --- a/thirdparty/php-peg/Parser.php +++ b/thirdparty/php-peg/Parser.php @@ -9,6 +9,13 @@ * the bracket if a failed match + restore has moved the current position backwards - so we have to check that too. */ class ParserRegexp { + + public $parser; + public $rx; + public $matches; + public $match_pos; + public $check_pos; + function __construct( $parser, $rx ) { $this->parser = $parser ; $this->rx = $rx . 'Sx' ;