NEW Create DBClassNameVarchar

This commit is contained in:
Steve Boyd 2024-09-10 14:27:04 +12:00
parent b13b657de5
commit a0ad753974
8 changed files with 209 additions and 122 deletions

View File

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

View File

@ -15,6 +15,7 @@ use SilverStripe\Dev\DevelopmentAdmin;
use SilverStripe\ORM\Connect\DatabaseException; use SilverStripe\ORM\Connect\DatabaseException;
use SilverStripe\ORM\Connect\TableBuilder; use SilverStripe\ORM\Connect\TableBuilder;
use SilverStripe\ORM\FieldType\DBClassName; use SilverStripe\ORM\FieldType\DBClassName;
use SilverStripe\ORM\FieldType\DBClassNameVarchar;
use SilverStripe\Security\Permission; use SilverStripe\Security\Permission;
use SilverStripe\Security\Security; use SilverStripe\Security\Security;
use SilverStripe\Versioned\Versioned; use SilverStripe\Versioned\Versioned;
@ -460,7 +461,8 @@ class DatabaseAdmin extends Controller
foreach ($dataClasses as $className) { foreach ($dataClasses as $className) {
$fieldSpecs = $schema->fieldSpecs($className); $fieldSpecs = $schema->fieldSpecs($className);
foreach ($fieldSpecs as $fieldName => $fieldSpec) { 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; $remapping[$className][] = $fieldName;
} }
} }

View File

@ -2,47 +2,15 @@
namespace SilverStripe\ORM\FieldType; namespace SilverStripe\ORM\FieldType;
use SilverStripe\Core\ClassInfo;
use SilverStripe\Core\Config\Config;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\DB; use SilverStripe\ORM\DB;
use SilverStripe\Core\Config\Config;
/** /**
* Represents a classname selector, which respects obsolete clasess. * Represents a classname selector, which respects obsolete clasess.
*/ */
class DBClassName extends DBEnum class DBClassName extends DBEnum
{ {
use 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);
}
/** /**
* @return void * @return void
@ -67,82 +35,6 @@ class DBClassName extends DBEnum
DB::require_field($this->getTable(), $this->getName(), $values); DB::require_field($this->getTable(), $this->getName(), $values);
} }
/**
* 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;
}
public function getDefault() public function getDefault()
{ {
// Check for assigned default // Check for assigned default
@ -151,15 +43,6 @@ class DBClassName extends DBEnum
return $default; return $default;
} }
// Allow classes to set default class return $this->getDefaultClassName();
$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);
} }
} }

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

@ -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(); DBEnum::flushCache();
$do1 = new TestObject(); $do1 = new TestObject();
$fields = $schema->databaseFields(TestObject::class, false); $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( $this->assertEquals(
[ [
TestObject::class, TestObject::class,

View File

@ -165,6 +165,11 @@ class DataObjectSchemaTest extends SapphireTest
public function testFieldSpec(array $args, array $expected): void public function testFieldSpec(array $args, array $expected): void
{ {
$schema = DataObject::getSchema(); $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)); $this->assertEquals($expected, $schema->fieldSpecs(...$args));
} }