/** * Return the DBField object that represents the given field. * This works similarly to obj() with 2 key differences: * - it still returns an object even when the field has no value. * - it only matches fields and not methods * - it matches foreign keys generated by has_one relationships, eg, "ParentID" * * @param string $fieldName Name of the field * @return DBField The field as a DBField object */ public function dbObject($fieldName) { // Check for field in DB $helper = static::getSchema()->fieldSpec(static::class, $fieldName, DataObjectSchema::INCLUDE_CLASS); if (!$helper) { return null; } $value = isset($this->record[$fieldName]) ? $this->record[$fieldName] : null; // If we have a DBField object in $this->record, then return that if ($value instanceof DBField) { return $value; } list($table, $spec) = explode('.', $helper); /** @var DBField $obj */ $obj = Object::create_from_string($spec, $fieldName); $obj->setTable($table); $obj->setValue($value, $this, false); return $obj; }
/** * Get the value of a field on this object, automatically inserting the value into any available casting objects * that have been specified. * * @param string $fieldName * @param array $arguments * @param bool $cache Cache this object * @param string $cacheName a custom cache name * @return Object|DBField */ public function obj($fieldName, $arguments = [], $cache = false, $cacheName = null) { if (!$cacheName && $cache) { $cacheName = $this->objCacheName($fieldName, $arguments); } // Check pre-cached value $value = $cache ? $this->objCacheGet($cacheName) : null; if ($value !== null) { return $value; } // Load value from record if ($this->hasMethod($fieldName)) { $value = call_user_func_array(array($this, $fieldName), $arguments ?: []); } else { $value = $this->{$fieldName}; } // Cast object if (!is_object($value)) { // Force cast $castingHelper = $this->castingHelper($fieldName); $valueObject = Object::create_from_string($castingHelper, $fieldName); $valueObject->setValue($value, $this); $value = $valueObject; } // Record in cache if ($cache) { $this->objCacheSet($cacheName, $value); } return $value; }
public function testCreate() { /** @var DBHTMLText $field */ $field = Object::create_from_string("HTMLFragment(['whitelist' => 'link'])", 'MyField'); $this->assertEquals(['link'], $field->getWhitelist()); $field = Object::create_from_string("HTMLFragment(['whitelist' => 'link,a'])", 'MyField'); $this->assertEquals(['link', 'a'], $field->getWhitelist()); $field = Object::create_from_string("HTMLFragment(['whitelist' => ['link', 'a']])", 'MyField'); $this->assertEquals(['link', 'a'], $field->getWhitelist()); $field = Object::create_from_string("HTMLFragment", 'MyField'); $this->assertEmpty($field->getWhitelist()); // Test shortcodes $field = Object::create_from_string("HTMLFragment(['shortcodes' => true])", 'MyField'); $this->assertEquals(true, $field->getProcessShortcodes()); $field = Object::create_from_string("HTMLFragment(['shortcodes' => false])", 'MyField'); $this->assertEquals(false, $field->getProcessShortcodes()); // Mix options $field = Object::create_from_string("HTMLFragment(['shortcodes' => true, 'whitelist' => ['a'])", 'MyField'); $this->assertEquals(true, $field->getProcessShortcodes()); $this->assertEquals(['a'], $field->getWhitelist()); }
/** * Get a db object for the named field * * @param string $field Field name * @return DBField|null */ public function dbObject($field) { $fields = $this->compositeDatabaseFields(); if (!isset($fields[$field])) { return null; } // Build nested field $key = $this->getName() . $field; $spec = $fields[$field]; /** @var DBField $fieldObject */ $fieldObject = Object::create_from_string($spec, $key); $fieldObject->setValue($this->getField($field), null, false); return $fieldObject; }
protected function constructExtensions() { $class = get_class($this); // Register this trait as a method source $this->registerExtraMethodCallback('defineExtensionMethods', function () { $this->defineExtensionMethods(); }); // Setup all extension instances for this instance foreach (ClassInfo::ancestry($class) as $class) { if (in_array($class, self::$unextendable_classes)) { continue; } $extensions = Config::inst()->get($class, 'extensions', Config::UNINHERITED | Config::EXCLUDE_EXTRA_SOURCES); if ($extensions) { foreach ($extensions as $extension) { $instance = Object::create_from_string($extension); $instance->setOwner(null, $class); $this->extension_instances[$instance->class] = $instance; } } } if (!isset(self::$classes_constructed[$class])) { $this->defineMethods(); self::$classes_constructed[$class] = true; } }
public function testDefault() { /** @var DBString $dbField */ $dbField = Object::create_from_string("StringFieldTest_MyStringField(['default' => 'Here is my default text'])", 'Myfield'); $this->assertEquals("Here is my default text", $dbField->getDefaultValue()); }
/** * Add an item to this many_many relationship * Does so by adding an entry to the joinTable. * * @throws InvalidArgumentException * @throws Exception * * @param DataObject|int $item * @param array $extraFields A map of additional columns to insert into the joinTable. * Column names should be ANSI quoted. * @throws Exception */ public function add($item, $extraFields = array()) { // Ensure nulls or empty strings are correctly treated as empty arrays if (empty($extraFields)) { $extraFields = array(); } // Determine ID of new record $itemID = null; if (is_numeric($item)) { $itemID = $item; } elseif ($item instanceof $this->dataClass) { $itemID = $item->ID; } else { throw new InvalidArgumentException("ManyManyList::add() expecting a {$this->dataClass} object, or ID value"); } if (empty($itemID)) { throw new InvalidArgumentException("ManyManyList::add() doesn't accept unsaved records"); } // Validate foreignID $foreignIDs = $this->getForeignID(); if (empty($foreignIDs)) { throw new BadMethodCallException("ManyManyList::add() can't be called until a foreign ID is set"); } // Apply this item to each given foreign ID record if (!is_array($foreignIDs)) { $foreignIDs = array($foreignIDs); } foreach ($foreignIDs as $foreignID) { // Check for existing records for this item if ($foreignFilter = $this->foreignIDWriteFilter($foreignID)) { // With the current query, simply add the foreign and local conditions // The query can be a bit odd, especially if custom relation classes // don't join expected tables (@see Member_GroupSet for example). $query = new SQLSelect("*", "\"{$this->joinTable}\""); $query->addWhere($foreignFilter); $query->addWhere(array("\"{$this->joinTable}\".\"{$this->localKey}\"" => $itemID)); $hasExisting = $query->count() > 0; } else { $hasExisting = false; } // Blank manipulation $manipulation = array($this->joinTable => array('command' => $hasExisting ? 'update' : 'insert', 'fields' => array())); if ($hasExisting) { $manipulation[$this->joinTable]['where'] = array("\"{$this->joinTable}\".\"{$this->foreignKey}\"" => $foreignID, "\"{$this->joinTable}\".\"{$this->localKey}\"" => $itemID); } if ($extraFields && $this->extraFields) { // Write extra field to manipluation in the same way // that DataObject::prepareManipulationTable writes fields foreach ($this->extraFields as $fieldName => $fieldSpec) { // Skip fields without an assignment if (array_key_exists($fieldName, $extraFields)) { $fieldObject = Object::create_from_string($fieldSpec, $fieldName); $fieldObject->setValue($extraFields[$fieldName]); $fieldObject->writeToManipulation($manipulation[$this->joinTable]); } } } $manipulation[$this->joinTable]['fields'][$this->localKey] = $itemID; $manipulation[$this->joinTable]['fields'][$this->foreignKey] = $foreignID; DB::manipulate($manipulation); } }
/** * Cache all database and composite fields for the given class. * Will do nothing if already cached * * @param string $class Class name to cache */ protected function cacheDatabaseFields($class) { // Skip if already cached if (isset($this->databaseFields[$class]) && isset($this->compositeFields[$class])) { return; } $compositeFields = array(); $dbFields = array(); // Ensure fixed fields appear at the start $fixedFields = DataObject::config()->get('fixed_fields'); if (get_parent_class($class) === DataObject::class) { // Merge fixed with ClassName spec and custom db fields $dbFields = $fixedFields; } else { $dbFields['ID'] = $fixedFields['ID']; } // Check each DB value as either a field or composite field $db = Config::inst()->get($class, 'db', Config::UNINHERITED) ?: array(); foreach ($db as $fieldName => $fieldSpec) { $fieldClass = strtok($fieldSpec, '('); if (singleton($fieldClass) instanceof DBComposite) { $compositeFields[$fieldName] = $fieldSpec; } else { $dbFields[$fieldName] = $fieldSpec; } } // Add in all has_ones $hasOne = Config::inst()->get($class, 'has_one', Config::UNINHERITED) ?: array(); foreach ($hasOne as $fieldName => $hasOneClass) { if ($hasOneClass === DataObject::class) { $compositeFields[$fieldName] = 'PolymorphicForeignKey'; } else { $dbFields["{$fieldName}ID"] = 'ForeignKey'; } } // Merge composite fields into DB foreach ($compositeFields as $fieldName => $fieldSpec) { $fieldObj = Object::create_from_string($fieldSpec, $fieldName); $fieldObj->setTable($class); $nestedFields = $fieldObj->compositeDatabaseFields(); foreach ($nestedFields as $nestedName => $nestedSpec) { $dbFields["{$fieldName}{$nestedName}"] = $nestedSpec; } } // Prevent field-less tables with only 'ID' if (count($dbFields) < 2) { $dbFields = []; } // Return cached results $this->databaseFields[$class] = $dbFields; $this->compositeFields[$class] = $compositeFields; }
/** * Generate the following table in the database, modifying whatever already exists * as necessary. * * @todo Change detection for CREATE TABLE $options other than "Engine" * * @param string $table The name of the table * @param array $fieldSchema A list of the fields to create, in the same form as DataObject::$db * @param array $indexSchema A list of indexes to create. See {@link requireIndex()} * The values of the array can be one of: * - true: Create a single column index on the field named the same as the index. * - array('fields' => array('A','B','C'), 'type' => 'index/unique/fulltext'): This gives you full * control over the index. * @param boolean $hasAutoIncPK A flag indicating that the primary key on this table is an autoincrement type * @param array $options Create table options (ENGINE, etc.) * @param array|bool $extensions List of extensions */ public function requireTable($table, $fieldSchema = null, $indexSchema = null, $hasAutoIncPK = true, $options = array(), $extensions = false) { if (!isset($this->tableList[strtolower($table)])) { $this->transCreateTable($table, $options, $extensions); $this->alterationMessage("Table {$table}: created", "created"); } else { if (Config::inst()->get(static::class, 'fix_table_case_on_build')) { $this->fixTableCase($table); } if (Config::inst()->get(static::class, 'check_and_repair_on_build')) { $this->checkAndRepairTable($table); } // Check if options changed $tableOptionsChanged = false; // Check for DB constant on the schema class $dbIDName = sprintf('%s::ID', get_class($this)); $dbID = defined($dbIDName) ? constant($dbIDName) : null; if ($dbID && isset($options[$dbID])) { if (preg_match('/ENGINE=([^\\s]*)/', $options[$dbID], $alteredEngineMatches)) { $alteredEngine = $alteredEngineMatches[1]; $tableStatus = $this->query(sprintf('SHOW TABLE STATUS LIKE \'%s\'', $table))->first(); $tableOptionsChanged = $tableStatus['Engine'] != $alteredEngine; } } if ($tableOptionsChanged || $extensions && $this->database->supportsExtensions($extensions)) { $this->transAlterTable($table, $options, $extensions); } } //DB ABSTRACTION: we need to convert this to a db-specific version: if (!isset($fieldSchema['ID'])) { $this->requireField($table, 'ID', $this->IdColumn(false, $hasAutoIncPK)); } // Create custom fields if ($fieldSchema) { foreach ($fieldSchema as $fieldName => $fieldSpec) { //Is this an array field? $arrayValue = ''; if (strpos($fieldSpec, '[') !== false) { //If so, remove it and store that info separately $pos = strpos($fieldSpec, '['); $arrayValue = substr($fieldSpec, $pos); $fieldSpec = substr($fieldSpec, 0, $pos); } /** @var DBField $fieldObj */ $fieldObj = Object::create_from_string($fieldSpec, $fieldName); $fieldObj->setArrayValue($arrayValue); $fieldObj->setTable($table); if ($fieldObj instanceof DBPrimaryKey) { /** @var DBPrimaryKey $fieldObj */ $fieldObj->setAutoIncrement($hasAutoIncPK); } $fieldObj->requireField(); } } // Create custom indexes if ($indexSchema) { foreach ($indexSchema as $indexName => $indexDetails) { $this->requireIndex($table, $indexName, $indexDetails); } } }
/** * Update the SELECT clause of the query with the columns from the given table * * @param SQLSelect $query * @param string $tableClass Class to select from * @param array $columns */ protected function selectColumnsFromTable(SQLSelect &$query, $tableClass, $columns = null) { // Add SQL for multi-value fields $schema = DataObject::getSchema(); $databaseFields = $schema->databaseFields($tableClass, false); $compositeFields = $schema->compositeFields($tableClass, false); unset($databaseFields['ID']); foreach ($databaseFields as $k => $v) { if ((is_null($columns) || in_array($k, $columns)) && !isset($compositeFields[$k])) { // Update $collidingFields if necessary $expressionForField = $query->expressionForField($k); $quotedField = $schema->sqlColumnForField($tableClass, $k); if ($expressionForField) { if (!isset($this->collidingFields[$k])) { $this->collidingFields[$k] = array($expressionForField); } $this->collidingFields[$k][] = $quotedField; } else { $query->selectField($quotedField, $k); } } } foreach ($compositeFields as $k => $v) { if ((is_null($columns) || in_array($k, $columns)) && $v) { $tableName = $schema->tableName($tableClass); $dbO = Object::create_from_string($v, $k); $dbO->setTable($tableName); $dbO->addToQuery($query); } } }