Merge branch '5' into 6

This commit is contained in:
Steve Boyd 2024-09-12 10:34:38 +12:00
commit 7ef517208c
12 changed files with 219 additions and 122 deletions

View File

@ -8,6 +8,8 @@ SilverStripe\Core\Injector\Injector:
class: SilverStripe\ORM\FieldType\DBCurrency
DBClassName:
class: SilverStripe\ORM\FieldType\DBClassName
DBClassNameVarchar:
class: SilverStripe\ORM\FieldType\DBClassNameVarchar
Date:
class: SilverStripe\ORM\FieldType\DBDate
Datetime:

View File

@ -225,7 +225,7 @@ class GridFieldDeleteAction extends AbstractGridFieldComponent implements GridFi
* @param string $columnName
* @return GridField_FormAction|null
*/
private function getRemoveAction($gridField, $record, $columnName)
protected function getRemoveAction($gridField, $record, $columnName)
{
if ($this->getRemoveRelation()) {
$this->checkForRequiredMethod($record, 'canEdit');

View File

@ -15,6 +15,7 @@ use SilverStripe\Dev\DevelopmentAdmin;
use SilverStripe\ORM\Connect\DatabaseException;
use SilverStripe\ORM\Connect\TableBuilder;
use SilverStripe\ORM\FieldType\DBClassName;
use SilverStripe\ORM\FieldType\DBClassNameVarchar;
use SilverStripe\Security\Permission;
use SilverStripe\Security\Security;
use SilverStripe\Versioned\Versioned;
@ -460,7 +461,8 @@ class DatabaseAdmin extends Controller
foreach ($dataClasses as $className) {
$fieldSpecs = $schema->fieldSpecs($className);
foreach ($fieldSpecs as $fieldName => $fieldSpec) {
if (Injector::inst()->create($fieldSpec, 'Dummy') instanceof DBClassName) {
$dummy = Injector::inst()->create($fieldSpec, 'Dummy');
if ($dummy instanceof DBClassName || $dummy instanceof DBClassNameVarchar) {
$remapping[$className][] = $fieldName;
}
}

View File

@ -2,42 +2,14 @@
namespace SilverStripe\ORM\FieldType;
use SilverStripe\Core\ClassInfo;
use SilverStripe\Core\Config\Config;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\DB;
use SilverStripe\View\ViewableData;
/**
* Represents a classname selector, which respects obsolete clasess.
*/
class DBClassName extends DBEnum
{
/**
* Base classname of class to enumerate.
* If 'DataObject' then all classes are included.
* If empty, then the baseClass of the parent object will be used
*/
protected ?string $baseClass = null;
/**
* Parent object
*/
protected ?DataObject $record = null;
private static string|bool $index = true;
/**
* Create a new DBClassName field
*
* @param string|null $baseClass Optional base class to limit selections
* @param array $options Optional parameters for this DBField instance
*/
public function __construct(?string $name = null, ?string $baseClass = null, array $options = [])
{
$this->setBaseClass($baseClass);
parent::__construct($name, null, null, $options);
}
use DBClassNameTrait;
public function requireField(): void
{
@ -59,73 +31,6 @@ class DBClassName extends DBEnum
DB::require_field($this->getTable(), $this->getName(), $values);
}
/**
* Get the base dataclass for the list of subclasses
*/
public function getBaseClass(): string
{
// Use explicit base class
if ($this->baseClass) {
return $this->baseClass;
}
// Default to the basename of the record
$schema = DataObject::getSchema();
if ($this->record) {
return $schema->baseDataClass($this->record);
}
// During dev/build only the table is assigned
$tableClass = $schema->tableClass($this->getTable());
if ($tableClass && ($baseClass = $schema->baseDataClass($tableClass))) {
return $baseClass;
}
// Fallback to global default
return DataObject::class;
}
/**
* Get the base name of the current class
* Useful as a non-fully qualified CSS Class name in templates.
*/
public function getShortName(): string
{
$value = $this->getValue();
if (empty($value) || !ClassInfo::exists($value)) {
return '';
}
return ClassInfo::shortName($value);
}
/**
* Assign the base class
*/
public function setBaseClass(?string $baseClass): static
{
$this->baseClass = $baseClass;
return $this;
}
/**
* Get list of classnames that should be selectable
*/
public function getEnum(): array
{
$classNames = ClassInfo::subclassesFor($this->getBaseClass());
$dataobject = strtolower(DataObject::class);
unset($classNames[$dataobject]);
return array_values($classNames ?? []);
}
public function setValue(mixed $value, null|array|ViewableData $record = null, bool $markChanged = true): static
{
parent::setValue($value, $record, $markChanged);
if ($record instanceof DataObject) {
$this->record = $record;
}
return $this;
}
public function getDefault(): string
{
// Check for assigned default
@ -134,15 +39,6 @@ class DBClassName extends DBEnum
return $default;
}
// Allow classes to set default class
$baseClass = $this->getBaseClass();
$defaultClass = Config::inst()->get($baseClass, 'default_classname');
if ($defaultClass && class_exists($defaultClass ?? '')) {
return $defaultClass;
}
// Fallback to first option
$enum = $this->getEnum();
return reset($enum);
return $this->getDefaultClassName();
}
}

View File

@ -0,0 +1,131 @@
<?php
namespace SilverStripe\ORM\FieldType;
use SilverStripe\Core\ClassInfo;
use SilverStripe\Core\Config\Config;
use SilverStripe\ORM\DataObject;
trait DBClassNameTrait
{
/**
* Base classname of class to enumerate.
* If 'DataObject' then all classes are included.
* If empty, then the baseClass of the parent object will be used
*
* @var string|null
*/
protected $baseClass = null;
/**
* Parent object
*
* @var DataObject|null
*/
protected $record = null;
private static $index = true;
/**
* Create a new DBClassName field
*
* @param string $name Name of field
* @param string|null $baseClass Optional base class to limit selections
* @param array $options Optional parameters for this DBField instance
*/
public function __construct($name = null, $baseClass = null, $options = [])
{
$this->setBaseClass($baseClass);
parent::__construct($name, null, null, $options);
}
/**
* Get the base dataclass for the list of subclasses
*
* @return string
*/
public function getBaseClass()
{
// Use explicit base class
if ($this->baseClass) {
return $this->baseClass;
}
// Default to the basename of the record
$schema = DataObject::getSchema();
if ($this->record) {
return $schema->baseDataClass($this->record);
}
// During dev/build only the table is assigned
$tableClass = $schema->tableClass($this->getTable());
if ($tableClass && ($baseClass = $schema->baseDataClass($tableClass))) {
return $baseClass;
}
// Fallback to global default
return DataObject::class;
}
/**
* Get the base name of the current class
* Useful as a non-fully qualified CSS Class name in templates.
*
* @return string|null
*/
public function getShortName()
{
$value = $this->getValue();
if (empty($value) || !ClassInfo::exists($value)) {
return null;
}
return ClassInfo::shortName($value);
}
/**
* Assign the base class
*
* @param string $baseClass
* @return $this
*/
public function setBaseClass($baseClass)
{
$this->baseClass = $baseClass;
return $this;
}
/**
* Get list of classnames that should be selectable
*
* @return array
*/
public function getEnum()
{
$classNames = ClassInfo::subclassesFor($this->getBaseClass());
$dataobject = strtolower(DataObject::class);
unset($classNames[$dataobject]);
return array_values($classNames ?? []);
}
public function setValue($value, $record = null, $markChanged = true)
{
parent::setValue($value, $record, $markChanged);
if ($record instanceof DataObject) {
$this->record = $record;
}
return $this;
}
private function getDefaultClassName()
{
// Allow classes to set default class
$baseClass = $this->getBaseClass();
$defaultClass = Config::inst()->get($baseClass, 'default_classname');
if ($defaultClass && class_exists($defaultClass ?? '')) {
return $defaultClass;
}
// Fallback to first option
$subClassNames = $this->getEnum();
return reset($subClassNames);
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace SilverStripe\ORM\FieldType;
use SilverStripe\ORM\FieldType\DBVarchar;
/**
* An alternative to DBClassName that stores the class name as a varchar instead of an enum
* This will use more disk space, though will prevent issues with long dev/builds on
* very large database tables when a ALTER TABLE queries are required to update the enum.
*
* Use the following config to use this class in your project:
*
* <code>
* SilverStripe\ORM\DataObject:
* fixed_fields:
* ClassName: DBClassNameVarchar
*
* SilverStripe\ORM\FieldType\DBPolymorphicForeignKey:
* composite_db:
* Class: DBClassNameVarchar('SilverStripe\ORM\DataObject', ['index' => false])
* </code>
*/
class DBClassNameVarchar extends DBVarchar
{
use DBClassNameTrait;
}

View File

@ -2,7 +2,7 @@
namespace SilverStripe\Core\Tests;
use ReflectionProperty;
use ReflectionClass;
use SilverStripe\Core\Environment;
use SilverStripe\Dev\SapphireTest;
@ -142,9 +142,8 @@ class EnvironmentTest extends SapphireTest
$this->assertSame($expected, Environment::hasEnv($name));
// unset the value
$reflectionEnv = new ReflectionProperty(Environment::class, 'env');
$reflectionEnv->setAccessible(true);
$reflectionEnv->setValue(array_diff($reflectionEnv->getValue(), [$name => $value]));
$reflectionEnv = new ReflectionClass(Environment::class);
$reflectionEnv->setStaticPropertyValue('env', array_diff($reflectionEnv->getStaticPropertyValue('env'), [$name => $value]));
unset($_ENV[$name]);
unset($_SERVER[$name]);
putenv("$name");

View File

@ -3,7 +3,7 @@
namespace SilverStripe\Core\Tests;
use BadMethodCallException;
use ReflectionProperty;
use ReflectionClass;
use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Tests\ExtensionTest\NamedExtension;
use SilverStripe\Dev\SapphireTest;
@ -15,9 +15,8 @@ class ExtensionTest extends SapphireTest
{
parent::setUp();
// Reset extra_methods so that when we set NamedExtension to null it re-evaluates which methods are available
$reflectionProperty = new ReflectionProperty(DataObject::class, 'extra_methods');
$reflectionProperty->setAccessible(true);
$reflectionProperty->setValue([]);
$reflectionClass = new ReflectionClass(DataObject::class);
$reflectionClass->setStaticPropertyValue('extra_methods', []);
// Add named extension config like we would in yaml
Config::modify()->merge(DataObject::class, 'extensions', ['NamedExtension' => NamedExtension::class]);
}

View File

@ -3,8 +3,8 @@
namespace SilverStripe\Logging\Tests;
use Monolog\Handler\HandlerInterface;
use ReflectionClass;
use ReflectionMethod;
use ReflectionProperty;
use SilverStripe\Control\Director;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Dev\Deprecation;
@ -156,8 +156,7 @@ class HTTPOutputHandlerTest extends SapphireTest
) {
$reflectionShouldShow = new ReflectionMethod(HTTPOutputHandler::class, 'shouldShowError');
$reflectionShouldShow->setAccessible(true);
$reflectionTriggeringError = new ReflectionProperty(Deprecation::class, 'isTriggeringError');
$reflectionTriggeringError->setAccessible(true);
$reflectionDeprecation = new ReflectionClass(Deprecation::class);
$cliShouldShowOrig = Deprecation::shouldShowForCli();
$httpShouldShowOrig = Deprecation::shouldShowForHttp();
@ -171,7 +170,7 @@ class HTTPOutputHandlerTest extends SapphireTest
Deprecation::setShouldShowForCli(true);
Deprecation::setShouldShowForHttp($shouldShow);
}
$reflectionTriggeringError->setValue($triggeringError);
$reflectionDeprecation->setStaticPropertyValue('isTriggeringError', $triggeringError);
$mockHandler = $this->getMockBuilder(HTTPOutputHandler::class)->onlyMethods(['isCli'])->getMock();
$mockHandler->method('isCli')->willReturn($isCli);
@ -181,6 +180,6 @@ class HTTPOutputHandlerTest extends SapphireTest
Deprecation::setShouldShowForCli($cliShouldShowOrig);
Deprecation::setShouldShowForHttp($httpShouldShowOrig);
$reflectionTriggeringError->setValue($triggeringErrorOrig);
$reflectionDeprecation->setStaticPropertyValue('isTriggeringError', $triggeringErrorOrig);
}
}

View File

@ -0,0 +1,35 @@
<?php
namespace SilverStripe\ORM\Tests;
use SilverStripe\Core\Config\Config;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\FieldType\DBClassNameVarchar;
use SilverStripe\ORM\FieldType\DBVarchar;
use SilverStripe\ORM\Tests\DataObjectSchemaTest\HasFields;
/**
* These unit tests test will change DBClassName to a varchar column
* and then test that the tests in DataObjectSchemaTest still pass
*
* There's also a test that a ClassName of an arbitary DataObject is a Varchar
*/
class DBClassNameVarcharTest extends DataObjectSchemaTest
{
public function setup(): void
{
parent::setup();
$fixedFields = Config::inst()->get(DataObject::class, 'fixed_fields');
$fixedFields['ClassName'] = 'DBClassNameVarchar';
Config::modify()->set(DataObject::class, 'fixed_fields', $fixedFields);
}
public function testVarcharType(): void
{
/** @var DataObject $obj */
$obj = HasFields::create();
$class = get_class($obj->dbObject('ClassName'));
$this->assertSame(DBClassNameVarchar::class, $class);
$this->assertTrue(is_a($class, DBVarchar::class, true));
}
}

View File

@ -200,7 +200,9 @@ class DataObjectSchemaGenerationTest extends SapphireTest
DBEnum::flushCache();
$do1 = new TestObject();
$fields = $schema->databaseFields(TestObject::class, false);
$this->assertEquals("DBClassName", $fields['ClassName']);
// May be overridden from DBClassName to DBClassNameVarchar by config
$expectedClassName = DataObject::config()->get('fixed_fields')['ClassName'];
$this->assertEquals($expectedClassName, $fields['ClassName']);
$this->assertEquals(
[
TestObject::class,

View File

@ -165,6 +165,11 @@ class DataObjectSchemaTest extends SapphireTest
public function testFieldSpec(array $args, array $expected): void
{
$schema = DataObject::getSchema();
// May be overridden from DBClassName to DBClassNameVarchar by config
$expectedClassName = DataObject::config()->get('fixed_fields')['ClassName'];
if (array_key_exists('ClassName', $expected) && $expectedClassName !== 'DBClassName') {
$expected['ClassName'] = str_replace('DBClassName', $expectedClassName, $expected['ClassName']);
}
$this->assertEquals($expected, $schema->fieldSpecs(...$args));
}