public function scaffoldFormField($title = null, $params = null) { if (empty($this->object)) { return null; } $relationName = substr($this->name, 0, -2); $hasOneClass = DataObject::getSchema()->hasOneComponent(get_class($this->object), $relationName); if (empty($hasOneClass)) { return null; } $hasOneSingleton = singleton($hasOneClass); if ($hasOneSingleton instanceof File) { $field = new UploadField($relationName, $title); if ($hasOneSingleton instanceof Image) { $field->setAllowedFileCategories('image/supported'); } return $field; } // Build selector / numeric field $titleField = $hasOneSingleton->hasField('Title') ? "Title" : "Name"; $list = DataList::create($hasOneClass); // Don't scaffold a dropdown for large tables, as making the list concrete // might exceed the available PHP memory in creating too many DataObject instances if ($list->count() < 100) { $field = new DropdownField($this->name, $title, $list->map('ID', $titleField)); $field->setEmptyString(' '); } else { $field = new NumericField($this->name, $title); } return $field; }
public function testApplyReplationDeepInheretence() { //test has_one relation $newDQ = new DataQuery('DataQueryTest_E'); //apply a relation to a relation from an ancestor class $newDQ->applyRelation('TestA'); $this->assertTrue($newDQ->query()->isJoinedTo('DataQueryTest_C')); $this->assertContains('"DataQueryTest_A"."ID" = "DataQueryTest_C"."TestAID"', $newDQ->sql($params)); //test many_many relation //test many_many with separate inheritance $newDQ = new DataQuery('DataQueryTest_C'); $baseDBTable = DataObject::getSchema()->baseDataTable('DataQueryTest_C'); $newDQ->applyRelation('ManyTestAs'); //check we are "joined" to the DataObject's table (there is no distinction between FROM or JOIN clauses) $this->assertTrue($newDQ->query()->isJoinedTo($baseDBTable)); //check we are explicitly selecting "FROM" the DO's table $this->assertContains("FROM \"{$baseDBTable}\"", $newDQ->sql()); //test many_many with shared inheritance $newDQ = new DataQuery('DataQueryTest_E'); $baseDBTable = DataObject::getSchema()->baseDataTable('DataQueryTest_E'); //check we are "joined" to the DataObject's table (there is no distinction between FROM or JOIN clauses) $this->assertTrue($newDQ->query()->isJoinedTo($baseDBTable)); //check we are explicitly selecting "FROM" the DO's table $this->assertContains("FROM \"{$baseDBTable}\"", $newDQ->sql(), 'The FROM clause is missing from the query'); $newDQ->applyRelation('ManyTestGs'); //confirm we are still joined to the base table $this->assertTrue($newDQ->query()->isJoinedTo($baseDBTable)); //double check it is the "FROM" clause $this->assertContains("FROM \"{$baseDBTable}\"", $newDQ->sql(), 'The FROM clause has been removed from the query'); //another (potentially less crude check) for checking "FROM" clause $fromTables = $newDQ->query()->getFrom(); $this->assertEquals('"' . $baseDBTable . '"', $fromTables[$baseDBTable]); }
/** * Test DataObject::composite_fields() and DataObject::is_composite_field() */ public function testCompositeFieldMetaDataFunctions() { $schema = DataObject::getSchema(); $this->assertEquals('Money', $schema->compositeField(DBCompositeTest_DataObject::class, 'MyMoney')); $this->assertNull($schema->compositeField(DBCompositeTest_DataObject::class, 'Title')); $this->assertEquals(array('MyMoney' => 'Money', 'OverriddenMoney' => 'Money'), $schema->compositeFields(DBCompositeTest_DataObject::class)); $this->assertEquals('Money', $schema->compositeField(SubclassedDBFieldObject::class, 'MyMoney')); $this->assertEquals('Money', $schema->compositeField(SubclassedDBFieldObject::class, 'OtherMoney')); $this->assertNull($schema->compositeField(SubclassedDBFieldObject::class, 'Title')); $this->assertNull($schema->compositeField(SubclassedDBFieldObject::class, 'OtherField')); $this->assertEquals(array('MyMoney' => 'Money', 'OtherMoney' => 'Money', 'OverriddenMoney' => 'Money'), $schema->compositeFields(SubclassedDBFieldObject::class)); }
protected function foreignIDFilter($id = null) { if ($id === null) { $id = $this->getForeignID(); } // Apply relation filter $key = DataObject::getSchema()->sqlColumnForField($this->dataClass(), $this->getForeignKey()); if (is_array($id)) { return array("{$key} IN (" . DB::placeholders($id) . ")" => $id); } else { if ($id !== null) { return array($key => $id); } } return null; }
/** * 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 'SilverStripe\\ORM\\DataObject'; }
/** * Check if allowed to upload more than one file * * @return bool */ public function getIsMultiUpload() { if (isset($this->multiUpload)) { return $this->multiUpload; } // Guess from record $record = $this->getRecord(); $name = $this->getName(); // Disabled for has_one components if ($record && DataObject::getSchema()->hasOneComponent(get_class($record), $name)) { return false; } return true; }
/** * Updates the database schema, creating tables & fields as necessary. * * @param boolean $quiet Don't show messages * @param boolean $populate Populate the database, as well as setting up its schema * @param bool $testMode */ public function doBuild($quiet = false, $populate = true, $testMode = false) { if ($quiet) { DB::quiet(); } else { $conn = DB::get_conn(); // Assumes database class is like "MySQLDatabase" or "MSSQLDatabase" (suffixed with "Database") $dbType = substr(get_class($conn), 0, -8); $dbVersion = $conn->getVersion(); $databaseName = method_exists($conn, 'currentDatabase') ? $conn->getSelectedDatabase() : ""; if (Director::is_cli()) { echo sprintf("\n\nBuilding database %s using %s %s\n\n", $databaseName, $dbType, $dbVersion); } else { echo sprintf("<h2>Building database %s using %s %s</h2>", $databaseName, $dbType, $dbVersion); } } // Set up the initial database if (!DB::is_active()) { if (!$quiet) { echo '<p><b>Creating database</b></p>'; } // Load parameters from existing configuration global $databaseConfig; if (empty($databaseConfig) && empty($_REQUEST['db'])) { user_error("No database configuration available", E_USER_ERROR); } $parameters = !empty($databaseConfig) ? $databaseConfig : $_REQUEST['db']; // Check database name is given if (empty($parameters['database'])) { user_error("No database name given; please give a value for \$databaseConfig['database']", E_USER_ERROR); } $database = $parameters['database']; // Establish connection and create database in two steps unset($parameters['database']); DB::connect($parameters); DB::create_database($database); } // Build the database. Most of the hard work is handled by DataObject $dataClasses = ClassInfo::subclassesFor('SilverStripe\\ORM\\DataObject'); array_shift($dataClasses); if (!$quiet) { if (Director::is_cli()) { echo "\nCREATING DATABASE TABLES\n\n"; } else { echo "\n<p><b>Creating database tables</b></p>\n\n"; } } // Initiate schema update $dbSchema = DB::get_schema(); $dbSchema->schemaUpdate(function () use($dataClasses, $testMode, $quiet) { foreach ($dataClasses as $dataClass) { // Check if class exists before trying to instantiate - this sidesteps any manifest weirdness if (!class_exists($dataClass)) { continue; } // Check if this class should be excluded as per testing conventions $SNG = singleton($dataClass); if (!$testMode && $SNG instanceof TestOnly) { continue; } // Log data if (!$quiet) { if (Director::is_cli()) { echo " * {$dataClass}\n"; } else { echo "<li>{$dataClass}</li>\n"; } } // Instruct the class to apply its schema to the database $SNG->requireTable(); } }); ClassInfo::reset_db_cache(); if ($populate) { if (!$quiet) { if (Director::is_cli()) { echo "\nCREATING DATABASE RECORDS\n\n"; } else { echo "\n<p><b>Creating database records</b></p>\n\n"; } } foreach ($dataClasses as $dataClass) { // Check if class exists before trying to instantiate - this sidesteps any manifest weirdness // Test_ indicates that it's the data class is part of testing system if (strpos($dataClass, 'Test_') === false && class_exists($dataClass)) { if (!$quiet) { if (Director::is_cli()) { echo " * {$dataClass}\n"; } else { echo "<li>{$dataClass}</li>\n"; } } singleton($dataClass)->requireDefaultRecords(); } } // Remap obsolete class names $schema = DataObject::getSchema(); foreach ($this->config()->classname_value_remapping as $oldClassName => $newClassName) { $badRecordCount = $newClassName::get()->filter(["ClassName" => $oldClassName])->count(); if ($badRecordCount > 0) { if (Director::is_cli()) { echo " * Correcting {$badRecordCount} obsolete classname values for {$newClassName}\n"; } else { echo "<li>Correcting {$badRecordCount} obsolete classname values for {$newClassName}</li>\n"; } $table = $schema->baseDataTable($newClassName); DB::prepared_query("UPDATE \"{$table}\" SET \"ClassName\" = ? WHERE \"ClassName\" = ?", [$newClassName, $oldClassName]); } } } touch(TEMP_FOLDER . '/database-last-generated-' . str_replace(array('\\', '/', ':'), '.', Director::baseFolder())); if (isset($_REQUEST['from_installer'])) { echo "OK"; } if (!$quiet) { echo Director::is_cli() ? "\n Database build completed!\n\n" : "<p>Database build completed!</p>"; } ClassInfo::reset_db_cache(); }
/** * Return children in the live site, if it exists. * * @param bool $showAll Include all of the elements, even those not shown in the menus. Only * applicable when extension is applied to {@link SiteTree}. * @param bool $onlyDeletedFromStage Only return items that have been deleted from stage * @return DataList * @throws Exception */ public function liveChildren($showAll = false, $onlyDeletedFromStage = false) { if (!$this->owner->hasExtension(Versioned::class)) { throw new Exception('Hierarchy->liveChildren() only works with Versioned extension applied'); } $baseClass = $this->owner->baseClass(); $hide_from_hierarchy = $this->owner->config()->hide_from_hierarchy; $hide_from_cms_tree = $this->owner->config()->hide_from_cms_tree; $children = $baseClass::get()->filter('ParentID', (int) $this->owner->ID)->exclude('ID', (int) $this->owner->ID)->setDataQueryParam(array('Versioned.mode' => $onlyDeletedFromStage ? 'stage_unique' : 'stage', 'Versioned.stage' => 'Live')); if ($hide_from_hierarchy) { $children = $children->exclude('ClassName', $hide_from_hierarchy); } if ($hide_from_cms_tree && $this->showingCMSTree()) { $children = $children->exclude('ClassName', $hide_from_cms_tree); } if (!$showAll && DataObject::getSchema()->fieldSpec($this->owner, 'ShowInMenus')) { $children = $children->filter('ShowInMenus', 1); } return $children; }
/** * @todo Better messages for relation checks and duplicate detection * Note that columnMap isn't used. * * @param array $record * @param array $columnMap * @param BulkLoader_Result $results * @param boolean $preview * * @return int */ protected function processRecord($record, $columnMap, &$results, $preview = false) { $class = $this->objectClass; // find existing object, or create new one $existingObj = $this->findExistingObject($record, $columnMap); /** @var DataObject $obj */ $obj = $existingObj ? $existingObj : new $class(); $schema = DataObject::getSchema(); // first run: find/create any relations and store them on the object // we can't combine runs, as other columns might rely on the relation being present foreach ($record as $fieldName => $val) { // don't bother querying of value is not set if ($this->isNullValue($val)) { continue; } // checking for existing relations if (isset($this->relationCallbacks[$fieldName])) { // trigger custom search method for finding a relation based on the given value // and write it back to the relation (or create a new object) $relationName = $this->relationCallbacks[$fieldName]['relationname']; /** @var DataObject $relationObj */ $relationObj = null; if ($this->hasMethod($this->relationCallbacks[$fieldName]['callback'])) { $relationObj = $this->{$this->relationCallbacks[$fieldName]['callback']}($obj, $val, $record); } elseif ($obj->hasMethod($this->relationCallbacks[$fieldName]['callback'])) { $relationObj = $obj->{$this->relationCallbacks[$fieldName]['callback']}($val, $record); } if (!$relationObj || !$relationObj->exists()) { $relationClass = $schema->hasOneComponent(get_class($obj), $relationName); $relationObj = new $relationClass(); //write if we aren't previewing if (!$preview) { $relationObj->write(); } } $obj->{"{$relationName}ID"} = $relationObj->ID; //write if we are not previewing if (!$preview) { $obj->write(); $obj->flushCache(); // avoid relation caching confusion } } elseif (strpos($fieldName, '.') !== false) { // we have a relation column with dot notation list($relationName, $columnName) = explode('.', $fieldName); // always gives us an component (either empty or existing) $relationObj = $obj->getComponent($relationName); if (!$preview) { $relationObj->write(); } $obj->{"{$relationName}ID"} = $relationObj->ID; //write if we are not previewing if (!$preview) { $obj->write(); $obj->flushCache(); // avoid relation caching confusion } } } // second run: save data foreach ($record as $fieldName => $val) { // break out of the loop if we are previewing if ($preview) { break; } // look up the mapping to see if this needs to map to callback $mapped = $this->columnMap && isset($this->columnMap[$fieldName]); if ($mapped && strpos($this->columnMap[$fieldName], '->') === 0) { $funcName = substr($this->columnMap[$fieldName], 2); $this->{$funcName}($obj, $val, $record); } else { if ($obj->hasMethod("import{$fieldName}")) { $obj->{"import{$fieldName}"}($val, $record); } else { $obj->update(array($fieldName => $val)); } } } // write record if (!$preview) { $obj->write(); } // @todo better message support $message = ''; // save to results if ($existingObj) { $results->addUpdated($obj, $message); } else { $results->addCreated($obj, $message); } $objID = $obj->ID; $obj->destroy(); // memory usage unset($existingObj); unset($obj); return $objID; }
/** * Return the specific version of the given id. * * Caution: The record is retrieved as a DataObject, but saving back * modifications via write() will create a new version, rather than * modifying the existing one. * * @param string $class * @param int $id * @param int $version * * @return DataObject */ public static function get_version($class, $id, $version) { $baseClass = DataObject::getSchema()->baseDataClass($class); $list = DataList::create($baseClass)->setDataQueryParam(["Versioned.mode" => 'version', "Versioned.version" => $version]); return $list->byID($id); }
/** * Adds table identifier to the every column. * Columns must have table identifier to prevent duplicate column name error. * * @param array $columns * @return string */ protected function prepareColumns($columns) { $cols = preg_split('/"?\\s*,\\s*"?/', trim($columns, '(") ')); $table = DataObject::getSchema()->tableForField($this->model, current($cols)); $cols = array_map(function ($col) use($table) { return sprintf('"%s"."%s"', $table, $col); }, $cols); return implode(',', $cols); }
public function testGetRemoteJoinField() { $schema = DataObject::getSchema(); // Company schema $staffJoinField = $schema->getRemoteJoinField(DataObjectTest_Company::class, 'CurrentStaff', 'has_many', $polymorphic); $this->assertEquals('CurrentCompanyID', $staffJoinField); $this->assertFalse($polymorphic, 'DataObjectTest_Company->CurrentStaff is not polymorphic'); $previousStaffJoinField = $schema->getRemoteJoinField(DataObjectTest_Company::class, 'PreviousStaff', 'has_many', $polymorphic); $this->assertEquals('PreviousCompanyID', $previousStaffJoinField); $this->assertFalse($polymorphic, 'DataObjectTest_Company->PreviousStaff is not polymorphic'); // CEO Schema $this->assertEquals('CEOID', $schema->getRemoteJoinField(DataObjectTest_CEO::class, 'Company', 'belongs_to', $polymorphic)); $this->assertFalse($polymorphic, 'DataObjectTest_CEO->Company is not polymorphic'); $this->assertEquals('PreviousCEOID', $schema->getRemoteJoinField(DataObjectTest_CEO::class, 'PreviousCompany', 'belongs_to', $polymorphic)); $this->assertFalse($polymorphic, 'DataObjectTest_CEO->PreviousCompany is not polymorphic'); // Team schema $this->assertEquals('Favourite', $schema->getRemoteJoinField(DataObjectTest_Team::class, 'Fans', 'has_many', $polymorphic)); $this->assertTrue($polymorphic, 'DataObjectTest_Team->Fans is polymorphic'); $this->assertEquals('TeamID', $schema->getRemoteJoinField(DataObjectTest_Team::class, 'Comments', 'has_many', $polymorphic)); $this->assertFalse($polymorphic, 'DataObjectTest_Team->Comments is not polymorphic'); }
/** * @param DataObject|DataObjectInterface $record */ public function saveInto(DataObjectInterface $record) { if (!isset($_FILES[$this->name])) { return; } $fileClass = File::get_class_for_file_extension(File::get_file_extension($_FILES[$this->name]['name'])); /** @var File $file */ if ($this->relationAutoSetting) { // assume that the file is connected via a has-one $objectClass = DataObject::getSchema()->hasOneComponent(get_class($record), $this->name); if ($objectClass === File::class || empty($objectClass)) { // Create object of the appropriate file class $file = Object::create($fileClass); } else { // try to create a file matching the relation $file = Object::create($objectClass); } } else { if ($record instanceof File) { $file = $record; } else { $file = Object::create($fileClass); } } $this->upload->loadIntoFile($_FILES[$this->name], $file, $this->getFolderName()); if ($this->upload->isError()) { return; } if ($this->relationAutoSetting) { if (empty($objectClass)) { return; } $file = $this->upload->getFile(); $record->{$this->name . 'ID'} = $file->ID; } }
/** * @todo move to SQLSelect * @todo fix hack */ protected function applyBaseTableFields() { $classes = ClassInfo::dataClassesFor($this->modelClass); $baseTable = DataObject::getSchema()->baseDataTable($this->modelClass); $fields = array("\"{$baseTable}\".*"); if ($this->modelClass != $classes[0]) { $fields[] = '"' . $classes[0] . '".*'; } //$fields = array_keys($model->db()); $fields[] = '"' . $classes[0] . '".\\"ClassName\\" AS "RecordClassName"'; return $fields; }
/** * The core search engine, used by this class and its subclasses to do fun stuff. * Searches both SiteTree and File. * * @param array $classesToSearch * @param string $keywords Keywords as a string. * @param int $start * @param int $pageLength * @param string $sortBy * @param string $extraFilter * @param bool $booleanSearch * @param string $alternativeFileFilter * @param bool $invertedMatch * @return PaginatedList * @throws Exception */ public function searchEngine($classesToSearch, $keywords, $start, $pageLength, $sortBy = "Relevance DESC", $extraFilter = "", $booleanSearch = false, $alternativeFileFilter = "", $invertedMatch = false) { $pageClass = 'SilverStripe\\CMS\\Model\\SiteTree'; $fileClass = 'File'; $pageTable = DataObject::getSchema()->tableName($pageClass); $fileTable = DataObject::getSchema()->tableName($fileClass); if (!class_exists($pageClass)) { throw new Exception('MySQLDatabase->searchEngine() requires "SiteTree" class'); } if (!class_exists($fileClass)) { throw new Exception('MySQLDatabase->searchEngine() requires "File" class'); } $keywords = $this->escapeString($keywords); $htmlEntityKeywords = htmlentities($keywords, ENT_NOQUOTES, 'UTF-8'); $extraFilters = array($pageClass => '', $fileClass => ''); if ($booleanSearch) { $boolean = "IN BOOLEAN MODE"; } if ($extraFilter) { $extraFilters[$pageClass] = " AND {$extraFilter}"; if ($alternativeFileFilter) { $extraFilters[$fileClass] = " AND {$alternativeFileFilter}"; } else { $extraFilters[$fileClass] = $extraFilters[$pageClass]; } } // Always ensure that only pages with ShowInSearch = 1 can be searched $extraFilters[$pageClass] .= " AND ShowInSearch <> 0"; // File.ShowInSearch was added later, keep the database driver backwards compatible // by checking for its existence first $fields = $this->getSchemaManager()->fieldList($fileTable); if (array_key_exists('ShowInSearch', $fields)) { $extraFilters[$fileClass] .= " AND ShowInSearch <> 0"; } $limit = $start . ", " . (int) $pageLength; $notMatch = $invertedMatch ? "NOT " : ""; if ($keywords) { $match[$pageClass] = "\n\t\t\t\tMATCH (Title, MenuTitle, Content, MetaDescription) AGAINST ('{$keywords}' {$boolean})\n\t\t\t\t+ MATCH (Title, MenuTitle, Content, MetaDescription) AGAINST ('{$htmlEntityKeywords}' {$boolean})\n\t\t\t"; $fileClassSQL = Convert::raw2sql($fileClass); $match[$fileClass] = "MATCH (Name, Title) AGAINST ('{$keywords}' {$boolean}) AND ClassName = '{$fileClassSQL}'"; // We make the relevance search by converting a boolean mode search into a normal one $relevanceKeywords = str_replace(array('*', '+', '-'), '', $keywords); $htmlEntityRelevanceKeywords = str_replace(array('*', '+', '-'), '', $htmlEntityKeywords); $relevance[$pageClass] = "MATCH (Title, MenuTitle, Content, MetaDescription) " . "AGAINST ('{$relevanceKeywords}') " . "+ MATCH (Title, MenuTitle, Content, MetaDescription) AGAINST ('{$htmlEntityRelevanceKeywords}')"; $relevance[$fileClass] = "MATCH (Name, Title) AGAINST ('{$relevanceKeywords}')"; } else { $relevance[$pageClass] = $relevance[$fileClass] = 1; $match[$pageClass] = $match[$fileClass] = "1 = 1"; } // Generate initial DataLists and base table names $lists = array(); $baseClasses = array($pageClass => '', $fileClass => ''); foreach ($classesToSearch as $class) { $lists[$class] = DataList::create($class)->where($notMatch . $match[$class] . $extraFilters[$class], ""); $baseClasses[$class] = '"' . $class . '"'; } $charset = Config::inst()->get('SilverStripe\\ORM\\Connect\\MySQLDatabase', 'charset'); // Make column selection lists $select = array($pageClass => array("ClassName", "{$pageTable}.\"ID\"", "ParentID", "Title", "MenuTitle", "URLSegment", "Content", "LastEdited", "Created", "Name" => "_{$charset}''", "Relevance" => $relevance[$pageClass], "CanViewType"), $fileClass => array("ClassName", "{$fileTable}.\"ID\"", "ParentID", "Title", "MenuTitle" => "_{$charset}''", "URLSegment" => "_{$charset}''", "Content" => "_{$charset}''", "LastEdited", "Created", "Name", "Relevance" => $relevance[$fileClass], "CanViewType" => "NULL")); // Process and combine queries $querySQLs = array(); $queryParameters = array(); $totalCount = 0; foreach ($lists as $class => $list) { $table = DataObject::getSchema()->tableName($class); /** @var SQLSelect $query */ $query = $list->dataQuery()->query(); // There's no need to do all that joining $query->setFrom($table); $query->setSelect($select[$class]); $query->setOrderBy(array()); $querySQLs[] = $query->sql($parameters); $queryParameters = array_merge($queryParameters, $parameters); $totalCount += $query->unlimitedRowCount(); } $fullQuery = implode(" UNION ", $querySQLs) . " ORDER BY {$sortBy} LIMIT {$limit}"; // Get records $records = $this->preparedQuery($fullQuery, $queryParameters); $objects = array(); foreach ($records as $record) { $objects[] = new $record['ClassName']($record); } $list = new PaginatedList(new ArrayList($objects)); $list->setPageStart($start); $list->setPageLength($pageLength); $list->setTotalItems($totalCount); // The list has already been limited by the query above $list->setLimitItems(false); return $list; }
/** * Update the permission set associated with $record DataObject * * @param DataObjectInterface $record */ public function saveInto(DataObjectInterface $record) { $fieldname = $this->name; $managedClass = $this->managedClass; // Remove all "privileged" permissions if the currently logged-in user is not an admin $privilegedPermissions = Permission::config()->privileged_permissions; if (!Permission::check('ADMIN')) { foreach ($this->value as $id => $bool) { if (in_array($id, $privilegedPermissions)) { unset($this->value[$id]); } } } // remove all permissions and re-add them afterwards $permissions = $record->{$fieldname}(); foreach ($permissions as $permission) { $permission->delete(); } $schema = DataObject::getSchema(); if ($fieldname && $record && ($schema->hasManyComponent(get_class($record), $fieldname) || $schema->manyManyComponent(get_class($record), $fieldname))) { if (!$record->ID) { $record->write(); } // We need a record ID to write permissions if ($this->value) { foreach ($this->value as $id => $bool) { if ($bool) { $perm = new $managedClass(); $perm->{$this->filterField} = $record->ID; $perm->Code = $id; $perm->write(); } } } } }
/** * @covers DataObjectSchema::baseDataClass() */ public function testBaseDataClass() { $schema = DataObject::getSchema(); $this->assertEquals('DataObjectSchemaTest_BaseClass', $schema->baseDataClass('DataObjectSchemaTest_BaseClass')); $this->assertEquals('DataObjectSchemaTest_BaseClass', $schema->baseDataClass('DataObjectSchemaTest_baseclass')); $this->assertEquals('DataObjectSchemaTest_BaseClass', $schema->baseDataClass('DataObjectSchemaTest_ChildClass')); $this->assertEquals('DataObjectSchemaTest_BaseClass', $schema->baseDataClass('DataObjectSchemaTest_CHILDCLASS')); $this->assertEquals('DataObjectSchemaTest_BaseClass', $schema->baseDataClass('DataObjectSchemaTest_GrandChildClass')); $this->assertEquals('DataObjectSchemaTest_BaseClass', $schema->baseDataClass('DataObjectSchemaTest_GRANDChildClass')); $this->setExpectedException('InvalidArgumentException'); $schema->baseDataClass('SilverStripe\\ORM\\DataObject'); }
/** * Build item resource from a changesetitem * * @param ChangeSetItem $changeSetItem * @return array */ protected function getChangeSetItemResource(ChangeSetItem $changeSetItem) { $baseClass = DataObject::getSchema()->baseDataClass($changeSetItem->ObjectClass); $baseSingleton = DataObject::singleton($baseClass); $thumbnailWidth = (int) $this->config()->thumbnail_width; $thumbnailHeight = (int) $this->config()->thumbnail_height; $hal = ['_links' => ['self' => ['href' => $this->ItemLink($changeSetItem->ID)]], 'ID' => $changeSetItem->ID, 'Created' => $changeSetItem->Created, 'LastEdited' => $changeSetItem->LastEdited, 'Title' => $changeSetItem->getTitle(), 'ChangeType' => $changeSetItem->getChangeType(), 'Added' => $changeSetItem->Added, 'ObjectClass' => $changeSetItem->ObjectClass, 'ObjectID' => $changeSetItem->ObjectID, 'BaseClass' => $baseClass, 'Singular' => $baseSingleton->i18n_singular_name(), 'Plural' => $baseSingleton->i18n_plural_name(), 'Thumbnail' => $changeSetItem->ThumbnailURL($thumbnailWidth, $thumbnailHeight)]; // Get preview urls $previews = $changeSetItem->getPreviewLinks(); if ($previews) { $hal['_links']['preview'] = $previews; } // Get edit link $editLink = $changeSetItem->CMSEditLink(); if ($editLink) { $hal['_links']['edit'] = ['href' => $editLink]; } // Depending on whether the object was added implicitly or explicitly, set // other related objects. if ($changeSetItem->Added === ChangeSetItem::IMPLICITLY) { $referencedItems = $changeSetItem->ReferencedBy(); $referencedBy = []; foreach ($referencedItems as $referencedItem) { $referencedBy[] = ['href' => $this->SetLink($referencedItem->ID)]; } if ($referencedBy) { $hal['_links']['referenced_by'] = $referencedBy; } } return $hal; }
/** * @param String $field Select statement identifier, either the unquoted column name, * the full composite SQL statement, or the alias set through {@link SQLSelect->selectField()}. * @return String The expression used to query this field via this DataQuery */ protected function expressionForField($field) { // Prepare query object for selecting this field $query = $this->getFinalisedQuery(array($field)); // Allow query to define the expression for this field $expression = $query->expressionForField($field); if (!empty($expression)) { return $expression; } // Special case for ID, if not provided if ($field === 'ID') { return DataObject::getSchema()->sqlColumnForField($this->dataClass, 'ID'); } return null; }
/** * Tests the generation of the ClassName spec and ensure it's not unnecessarily influenced * by the order of classnames of existing records */ public function testClassNameSpecGeneration() { $schema = DataObject::getSchema(); // Test with blank entries DBClassName::clear_classname_cache(); $do1 = new DataObjectSchemaGenerationTest_DO(); $fields = $schema->databaseFields(DataObjectSchemaGenerationTest_DO::class, false); /** @skipUpgrade */ $this->assertEquals("DBClassName", $fields['ClassName']); $this->assertEquals(array('DataObjectSchemaGenerationTest_DO' => 'DataObjectSchemaGenerationTest_DO', 'DataObjectSchemaGenerationTest_IndexDO' => 'DataObjectSchemaGenerationTest_IndexDO'), $do1->dbObject('ClassName')->getEnum()); // Test with instance of subclass $item1 = new DataObjectSchemaGenerationTest_IndexDO(); $item1->write(); DBClassName::clear_classname_cache(); $this->assertEquals(array('DataObjectSchemaGenerationTest_DO' => 'DataObjectSchemaGenerationTest_DO', 'DataObjectSchemaGenerationTest_IndexDO' => 'DataObjectSchemaGenerationTest_IndexDO'), $item1->dbObject('ClassName')->getEnum()); $item1->delete(); // Test with instance of main class $item2 = new DataObjectSchemaGenerationTest_DO(); $item2->write(); DBClassName::clear_classname_cache(); $this->assertEquals(array('DataObjectSchemaGenerationTest_DO' => 'DataObjectSchemaGenerationTest_DO', 'DataObjectSchemaGenerationTest_IndexDO' => 'DataObjectSchemaGenerationTest_IndexDO'), $item2->dbObject('ClassName')->getEnum()); $item2->delete(); // Test with instances of both classes $item1 = new DataObjectSchemaGenerationTest_IndexDO(); $item1->write(); $item2 = new DataObjectSchemaGenerationTest_DO(); $item2->write(); DBClassName::clear_classname_cache(); $this->assertEquals(array('DataObjectSchemaGenerationTest_DO' => 'DataObjectSchemaGenerationTest_DO', 'DataObjectSchemaGenerationTest_IndexDO' => 'DataObjectSchemaGenerationTest_IndexDO'), $item1->dbObject('ClassName')->getEnum()); $item1->delete(); $item2->delete(); }
public function testRelationParsing() { $schema = DataObject::getSchema(); // Parent components $this->assertEquals([ManyManyThroughList::class, ManyManyThroughListTest_Object::class, ManyManyThroughListTest_Item::class, 'ParentID', 'ChildID', ManyManyThroughListTest_JoinObject::class], $schema->manyManyComponent(ManyManyThroughListTest_Object::class, 'Items')); // Belongs_many_many is the same, but with parent/child substituted $this->assertEquals([ManyManyThroughList::class, ManyManyThroughListTest_Item::class, ManyManyThroughListTest_Object::class, 'ChildID', 'ParentID', ManyManyThroughListTest_JoinObject::class], $schema->manyManyComponent(ManyManyThroughListTest_Item::class, 'Objects')); }
/** * Returns the header row providing titles with sort buttons * * @param GridField $gridField * @return array */ public function getHTMLFragments($gridField) { $list = $gridField->getList(); if (!$this->checkDataType($list)) { return null; } /** @var Sortable $list */ $forTemplate = new ArrayData(array()); $forTemplate->Fields = new ArrayList(); $state = $gridField->State->GridFieldSortableHeader; $columns = $gridField->getColumns(); $currentColumn = 0; $schema = DataObject::getSchema(); foreach ($columns as $columnField) { $currentColumn++; $metadata = $gridField->getColumnMetadata($columnField); $fieldName = str_replace('.', '-', $columnField); $title = $metadata['title']; if (isset($this->fieldSorting[$columnField]) && $this->fieldSorting[$columnField]) { $columnField = $this->fieldSorting[$columnField]; } $allowSort = $title && $list->canSortBy($columnField); if (!$allowSort && strpos($columnField, '.') !== false) { // we have a relation column with dot notation // @see DataObject::relField for approximation $parts = explode('.', $columnField); $tmpItem = singleton($list->dataClass()); for ($idx = 0; $idx < sizeof($parts); $idx++) { $methodName = $parts[$idx]; if ($tmpItem instanceof SS_List) { // It's impossible to sort on a HasManyList/ManyManyList break; } elseif (method_exists($tmpItem, 'hasMethod') && $tmpItem->hasMethod($methodName)) { // The part is a relation name, so get the object/list from it $tmpItem = $tmpItem->{$methodName}(); } elseif ($tmpItem instanceof DataObject && $schema->fieldSpec($tmpItem, $methodName, DataObjectSchema::DB_ONLY)) { // Else, if we've found a database field at the end of the chain, we can sort on it. // If a method is applied further to this field (E.g. 'Cost.Currency') then don't try to sort. $allowSort = $idx === sizeof($parts) - 1; break; } else { // If neither method nor field, then unable to sort break; } } } if ($allowSort) { $dir = 'asc'; if ($state->SortColumn(null) == $columnField && $state->SortDirection('asc') == 'asc') { $dir = 'desc'; } $field = GridField_FormAction::create($gridField, 'SetOrder' . $fieldName, $title, "sort{$dir}", array('SortColumn' => $columnField))->addExtraClass('grid-field__sort'); if ($state->SortColumn(null) == $columnField) { $field->addExtraClass('ss-gridfield-sorted'); if ($state->SortDirection('asc') == 'asc') { $field->addExtraClass('ss-gridfield-sorted-asc'); } else { $field->addExtraClass('ss-gridfield-sorted-desc'); } } } else { if ($currentColumn == count($columns) && $gridField->getConfig()->getComponentByType('SilverStripe\\Forms\\GridField\\GridFieldFilterHeader')) { $field = new LiteralField($fieldName, '<button type="button" name="showFilter" class="btn font-icon-search btn--no-text btn--icon-large grid-field__filter-open"></button>'); } else { $field = new LiteralField($fieldName, '<span class="non-sortable">' . $title . '</span>'); } } $forTemplate->Fields->push($field); } $template = SSViewer::get_templates_by_class($this, '_Row', __CLASS__); return array('header' => $forTemplate->renderWith($template)); }
/** * Merges data and relations from another object of same class, * without conflict resolution. Allows to specify which * dataset takes priority in case its not empty. * has_one-relations are just transferred with priority 'right'. * has_many and many_many-relations are added regardless of priority. * * Caution: has_many/many_many relations are moved rather than duplicated, * meaning they are not connected to the merged object any longer. * Caution: Just saves updated has_many/many_many relations to the database, * doesn't write the updated object itself (just writes the object-properties). * Caution: Does not delete the merged object. * Caution: Does now overwrite Created date on the original object. * * @param DataObject $rightObj * @param string $priority left|right Determines who wins in case of a conflict (optional) * @param bool $includeRelations Merge any existing relations (optional) * @param bool $overwriteWithEmpty Overwrite existing left values with empty right values. * Only applicable with $priority='right'. (optional) * @return Boolean */ public function merge($rightObj, $priority = 'right', $includeRelations = true, $overwriteWithEmpty = false) { $leftObj = $this; if ($leftObj->ClassName != $rightObj->ClassName) { // we can't merge similiar subclasses because they might have additional relations user_error("DataObject->merge(): Invalid object class '{$rightObj->ClassName}'\n\t\t\t(expected '{$leftObj->ClassName}').", E_USER_WARNING); return false; } if (!$rightObj->ID) { user_error("DataObject->merge(): Please write your merged-in object to the database before merging,\n\t\t\t\tto make sure all relations are transferred properly.').", E_USER_WARNING); return false; } // makes sure we don't merge data like ID or ClassName $rightData = DataObject::getSchema()->fieldSpecs(get_class($rightObj)); foreach ($rightData as $key => $rightSpec) { // Don't merge ID if ($key === 'ID') { continue; } // Only merge relations if allowed if ($rightSpec === 'ForeignKey' && !$includeRelations) { continue; } // don't merge conflicting values if priority is 'left' if ($priority == 'left' && $leftObj->{$key} !== $rightObj->{$key}) { continue; } // don't overwrite existing left values with empty right values (if $overwriteWithEmpty is set) if ($priority == 'right' && !$overwriteWithEmpty && empty($rightObj->{$key})) { continue; } // TODO remove redundant merge of has_one fields $leftObj->{$key} = $rightObj->{$key}; } // merge relations if ($includeRelations) { if ($manyMany = $this->manyMany()) { foreach ($manyMany as $relationship => $class) { $leftComponents = $leftObj->getManyManyComponents($relationship); $rightComponents = $rightObj->getManyManyComponents($relationship); if ($rightComponents && $rightComponents->exists()) { $leftComponents->addMany($rightComponents->column('ID')); } $leftComponents->write(); } } if ($hasMany = $this->hasMany()) { foreach ($hasMany as $relationship => $class) { $leftComponents = $leftObj->getComponents($relationship); $rightComponents = $rightObj->getComponents($relationship); if ($rightComponents && $rightComponents->exists()) { $leftComponents->addMany($rightComponents->column('ID')); } $leftComponents->write(); } } } return true; }
/** * Determine maximum number of files allowed to be attached * Defaults to 1 for has_one and null (unlimited) for * many_many and has_many relations. * * @return integer|null Maximum limit, or null for no limit */ public function getAllowedMaxFileNumber() { $allowedMaxFileNumber = $this->getConfig('allowedMaxFileNumber'); // if there is a has_one relation with that name on the record and // allowedMaxFileNumber has not been set, it's wanted to be 1 if (empty($allowedMaxFileNumber)) { $record = $this->getRecord(); $name = $this->getName(); if ($record && DataObject::getSchema()->hasOneComponent(get_class($record), $name)) { return 1; // Default for has_one } else { return null; // Default for has_many and many_many } } else { return $allowedMaxFileNumber; } }
protected function overrideField($obj, $fieldName, $value, $fixtures = null) { $class = get_class($obj); $table = DataObject::getSchema()->tableForField($class, $fieldName); $value = $this->parseValue($value, $fixtures); DB::manipulate(array($table => array("command" => "update", "id" => $obj->ID, "class" => $class, "fields" => array($fieldName => $value)))); $obj->{$fieldName} = $value; }
/** * Checks the database is in a state to perform security checks. * See {@link DatabaseAdmin->init()} for more information. * * @return bool */ public static function database_is_ready() { // Used for unit tests if (self::$force_database_is_ready !== null) { return self::$force_database_is_ready; } if (self::$database_is_ready) { return self::$database_is_ready; } $requiredClasses = ClassInfo::dataClassesFor('SilverStripe\\Security\\Member'); $requiredClasses[] = 'SilverStripe\\Security\\Group'; $requiredClasses[] = 'SilverStripe\\Security\\Permission'; foreach ($requiredClasses as $class) { // Skip test classes, as not all test classes are scaffolded at once if (is_subclass_of($class, 'TestOnly')) { continue; } // if any of the tables aren't created in the database $table = DataObject::getSchema()->tableName($class); if (!ClassInfo::hasTable($table)) { return false; } // HACK: DataExtensions aren't applied until a class is instantiated for // the first time, so create an instance here. singleton($class); // if any of the tables don't have all fields mapped as table columns $dbFields = DB::field_list($table); if (!$dbFields) { return false; } $objFields = DataObject::database_fields($class); $missingFields = array_diff_key($objFields, $dbFields); if ($missingFields) { return false; } } self::$database_is_ready = true; return true; }
/** * @deprecated 4.0..5.0 */ public static function table_for_object_field($candidateClass, $fieldName) { Deprecation::notice('5.0', 'Use DataObject::getSchema()->tableForField()'); return DataObject::getSchema()->tableForField($candidateClass, $fieldName); }
/** * Remove all items from this many-many join. To remove a subset of items, * filter it first. * * @return void */ public function removeAll() { // Remove the join to the join table to avoid MySQL row locking issues. $query = $this->dataQuery(); $foreignFilter = $query->getQueryParam('Foreign.Filter'); $query->removeFilterOn($foreignFilter); // Select ID column $selectQuery = $query->query(); $dataClassIDColumn = DataObject::getSchema()->sqlColumnForField($this->dataClass(), 'ID'); $selectQuery->setSelect($dataClassIDColumn); $from = $selectQuery->getFrom(); unset($from[$this->joinTable]); $selectQuery->setFrom($from); $selectQuery->setOrderBy(); // ORDER BY in subselects breaks MS SQL Server and is not necessary here $selectQuery->setDistinct(false); // Use a sub-query as SQLite does not support setting delete targets in // joined queries. $delete = new SQLDelete(); $delete->setFrom("\"{$this->joinTable}\""); $delete->addWhere($this->foreignIDFilter()); $subSelect = $selectQuery->sql($parameters); $delete->addWhere(array("\"{$this->joinTable}\".\"{$this->localKey}\" IN ({$subSelect})" => $parameters)); $delete->execute(); }
/** * Return a list of all tuples attached to this dataobject * Note: Variants are excluded * * @param DataObject $record to search * @return array */ protected function findAssets(DataObject $record) { // Search for dbfile instances $files = array(); $fields = DataObject::getSchema()->fieldSpecs($record); foreach ($fields as $field => $db) { $fieldObj = $record->{$field}; if (!$fieldObj instanceof DBFile) { continue; } // Omit variant and merge with set $next = $record->dbObject($field)->getValue(); unset($next['Variant']); if ($next) { $files[] = $next; } } // De-dupe return array_map("unserialize", array_unique(array_map("serialize", $files))); }
/** * Remove invalid records from tables - that is, records that don't have * corresponding records in their parent class tables. */ public function cleanup() { $baseClasses = []; foreach (ClassInfo::subclassesFor(DataObject::class) as $class) { if (get_parent_class($class) == DataObject::class) { $baseClasses[] = $class; } } $schema = DataObject::getSchema(); foreach ($baseClasses as $baseClass) { // Get data classes $baseTable = $schema->baseDataTable($baseClass); $subclasses = ClassInfo::subclassesFor($baseClass); unset($subclasses[0]); foreach ($subclasses as $k => $subclass) { if (!DataObject::getSchema()->classHasTable($subclass)) { unset($subclasses[$k]); } } if ($subclasses) { $records = DB::query("SELECT * FROM \"{$baseTable}\""); foreach ($subclasses as $subclass) { $subclassTable = $schema->tableName($subclass); $recordExists[$subclass] = DB::query("SELECT \"ID\" FROM \"{$subclassTable}\"")->keyedColumn(); } foreach ($records as $record) { foreach ($subclasses as $subclass) { $subclassTable = $schema->tableName($subclass); $id = $record['ID']; if ($record['ClassName'] != $subclass && !is_subclass_of($record['ClassName'], $subclass) && isset($recordExists[$subclass][$id])) { $sql = "DELETE FROM \"{$subclassTable}\" WHERE \"ID\" = ?"; echo "<li>{$sql} [{$id}]</li>"; DB::prepared_query($sql, [$id]); } } } } } }