mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
NEW DataObject related objects service
This commit is contained in:
parent
cf79be8e2c
commit
6e77d5eada
6
_config/relateddata.yml
Normal file
6
_config/relateddata.yml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
Name: relateddata
|
||||||
|
---
|
||||||
|
SilverStripe\Core\Injector\Injector:
|
||||||
|
SilverStripe\ORM\RelatedData\RelatedDataService:
|
||||||
|
class: SilverStripe\ORM\RelatedData\StandardRelatedDataService
|
@ -26,6 +26,7 @@ use SilverStripe\ORM\FieldType\DBField;
|
|||||||
use SilverStripe\ORM\Filters\SearchFilter;
|
use SilverStripe\ORM\Filters\SearchFilter;
|
||||||
use SilverStripe\ORM\Queries\SQLDelete;
|
use SilverStripe\ORM\Queries\SQLDelete;
|
||||||
use SilverStripe\ORM\Search\SearchContext;
|
use SilverStripe\ORM\Search\SearchContext;
|
||||||
|
use SilverStripe\ORM\RelatedData\RelatedDataService;
|
||||||
use SilverStripe\ORM\UniqueKey\UniqueKeyInterface;
|
use SilverStripe\ORM\UniqueKey\UniqueKeyInterface;
|
||||||
use SilverStripe\ORM\UniqueKey\UniqueKeyService;
|
use SilverStripe\ORM\UniqueKey\UniqueKeyService;
|
||||||
use SilverStripe\Security\Member;
|
use SilverStripe\Security\Member;
|
||||||
@ -4352,4 +4353,20 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
|||||||
{
|
{
|
||||||
return $this->extend('cacheKeyComponent');
|
return $this->extend('cacheKeyComponent');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find all other DataObject instances that are related to this DataObject in the database
|
||||||
|
* through has_one and many_many relationships. For example:
|
||||||
|
* This method is called on a File. The MyPage model $has_one File. There is a Page record that has
|
||||||
|
* a FileID = $this->ID. This SS_List returned by this method will include that Page instance.
|
||||||
|
*
|
||||||
|
* @param string[] $excludedClasses
|
||||||
|
* @return SS_List
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
public function findAllRelatedData(array $excludedClasses = []): SS_List
|
||||||
|
{
|
||||||
|
$service = Injector::inst()->get(RelatedDataService::class);
|
||||||
|
return $service->findAll($this, $excludedClasses);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
25
src/ORM/RelatedData/RelatedDataService.php
Normal file
25
src/ORM/RelatedData/RelatedDataService.php
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\ORM\RelatedData;
|
||||||
|
|
||||||
|
use SilverStripe\ORM\DataObject;
|
||||||
|
use SilverStripe\ORM\SS_List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface used to find all other DataObject instances that are related to a DataObject instance
|
||||||
|
* in the database
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
interface RelatedDataService
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find all DataObject instances that have a linked relationship with $record
|
||||||
|
*
|
||||||
|
* @param DataObject $record
|
||||||
|
* @param string[] $excludedClasses
|
||||||
|
* @return SS_List
|
||||||
|
*/
|
||||||
|
public function findAll(DataObject $record, array $excludedClasses = []): SS_List;
|
||||||
|
}
|
507
src/ORM/RelatedData/StandardRelatedDataService.php
Normal file
507
src/ORM/RelatedData/StandardRelatedDataService.php
Normal file
@ -0,0 +1,507 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\ORM\RelatedData;
|
||||||
|
|
||||||
|
use ReflectionException;
|
||||||
|
use SilverStripe\Core\ClassInfo;
|
||||||
|
use SilverStripe\Core\Config\Config;
|
||||||
|
use SilverStripe\ORM\DB;
|
||||||
|
use SilverStripe\ORM\SS_List;
|
||||||
|
use SilverStripe\ORM\ArrayList;
|
||||||
|
use SilverStripe\ORM\Connect\Query;
|
||||||
|
use SilverStripe\ORM\DataObject;
|
||||||
|
use SilverStripe\ORM\DataObjectSchema;
|
||||||
|
use SilverStripe\ORM\Queries\SQLSelect;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service class used to find all other DataObject instances that are related to a DataObject instance
|
||||||
|
* in the database
|
||||||
|
*
|
||||||
|
* Example demonstrating what '$component' and '$componentClassName' variables refer to:
|
||||||
|
* PHP model: private static $has_one = [ 'MyFile' => File::class ]
|
||||||
|
* - $component: 'MyFile'
|
||||||
|
* - $componentClassName: SilverStripe\Assets\File::class
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
class StandardRelatedDataService implements RelatedDataService
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to prevent duplicate database queries
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
private $queryIdens = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
private $config;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var DataObjectSchema
|
||||||
|
*/
|
||||||
|
private $dataObjectSchema;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
private $classToTableName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find all DataObject instances that have a linked relationship with $record
|
||||||
|
*
|
||||||
|
* @param DataObject $record
|
||||||
|
* @param string[] $excludedClasses
|
||||||
|
* @return SS_List
|
||||||
|
*/
|
||||||
|
public function findAll(DataObject $record, array $excludedClasses = []): SS_List
|
||||||
|
{
|
||||||
|
// Do not query unsaved DataObjects
|
||||||
|
if (!$record->exists()) {
|
||||||
|
return ArrayList::create();
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->config = Config::inst()->getAll();
|
||||||
|
$this->dataObjectSchema = DataObjectSchema::create();
|
||||||
|
$this->initClassToTableName();
|
||||||
|
$classIDs = [];
|
||||||
|
$throughClasses = [];
|
||||||
|
|
||||||
|
// "regular" relations i.e. point from $record to different DataObject
|
||||||
|
$this->addRelatedHasOnes($classIDs, $record);
|
||||||
|
$this->addRelatedManyManys($classIDs, $record, $throughClasses);
|
||||||
|
|
||||||
|
// Loop config data to find "reverse" relationships pointing back to $record
|
||||||
|
foreach (array_keys($this->config) as $lowercaseClassName) {
|
||||||
|
if (!class_exists($lowercaseClassName)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Example of $class: My\App\MyPage (extends SiteTree)
|
||||||
|
try {
|
||||||
|
$class = ClassInfo::class_name($lowercaseClassName);
|
||||||
|
} catch (ReflectionException $e) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!is_subclass_of($class, DataObject::class)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$this->addRelatedReverseHasOnes($classIDs, $record, $class);
|
||||||
|
$this->addRelatedReverseManyManys($classIDs, $record, $class, $throughClasses);
|
||||||
|
}
|
||||||
|
$this->removeClasses($classIDs, $excludedClasses, $throughClasses);
|
||||||
|
$classObjs = $this->fetchClassObjs($classIDs);
|
||||||
|
return $this->deriveList($classIDs, $classObjs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loop has_one relationships on the DataObject we're getting usage for
|
||||||
|
* e.g. File.has_one = Page, Page.has_many = File
|
||||||
|
*
|
||||||
|
* @param array $classIDs
|
||||||
|
* @param DataObject $record
|
||||||
|
*/
|
||||||
|
private function addRelatedHasOnes(array &$classIDs, DataObject $record): void
|
||||||
|
{
|
||||||
|
$class = get_class($record);
|
||||||
|
foreach ($record->hasOne() as $component => $componentClass) {
|
||||||
|
$componentIDField = "{$component}ID";
|
||||||
|
$tableName = $this->findTableNameContainingComponentIDField($class, $componentIDField);
|
||||||
|
if ($tableName === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$select = sprintf('"%s"', $componentIDField);
|
||||||
|
$where = sprintf('"ID" = %u AND "%s" > 0', $record->ID, $componentIDField);
|
||||||
|
|
||||||
|
// Polymorphic
|
||||||
|
// $record->ParentClass will return null if the column doesn't exist
|
||||||
|
if ($componentIDField === 'ParentID' && $record->ParentClass) {
|
||||||
|
$select .= ', "ParentClass"';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent duplicate counting of self-referential relations
|
||||||
|
// The relation will still be fetched by $this::fetchReverseHasOneResults()
|
||||||
|
if ($record instanceof $componentClass) {
|
||||||
|
$where .= sprintf(' AND "%s" != %u', $componentIDField, $record->ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example SQL:
|
||||||
|
// Normal:
|
||||||
|
// SELECT "MyPageID" FROM "MyFile" WHERE "ID" = 789 AND "MyPageID" > 0;
|
||||||
|
// Prevent self-referential e.g. File querying File:
|
||||||
|
// SELECT "MyFileSubClassID" FROM "MyFile" WHERE "ID" = 456
|
||||||
|
// AND "MyFileSubClassID" > 0 AND MyFileSubClassID != 456;
|
||||||
|
// Polymorphic:
|
||||||
|
// SELECT "ParentID", "ParentClass" FROM "MyFile" WHERE "ID" = 789 AND "ParentID" > 0;
|
||||||
|
$results = SQLSelect::create(
|
||||||
|
$select,
|
||||||
|
sprintf('"%s"', $tableName),
|
||||||
|
$where
|
||||||
|
)->execute();
|
||||||
|
$this->addResultsToClassIDs($classIDs, $results, $componentClass);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the table that contains $componentIDField - this is relevant for subclassed DataObjects
|
||||||
|
* that live in the database as two tables that are joined together
|
||||||
|
*
|
||||||
|
* @param string $class
|
||||||
|
* @param string $componentIDField
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private function findTableNameContainingComponentIDField(string $class, string $componentIDField): string
|
||||||
|
{
|
||||||
|
$tableName = '';
|
||||||
|
$candidateClass = $class;
|
||||||
|
while ($candidateClass) {
|
||||||
|
$dbFields = $this->dataObjectSchema->databaseFields($candidateClass, false);
|
||||||
|
if (array_key_exists($componentIDField, $dbFields)) {
|
||||||
|
$tableName = $this->dataObjectSchema->tableName($candidateClass);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
$candidateClass = get_parent_class($class);
|
||||||
|
}
|
||||||
|
return $tableName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loop many_many relationships on the DataObject we're getting usage for
|
||||||
|
*
|
||||||
|
* @param array $classIDs
|
||||||
|
* @param DataObject $record
|
||||||
|
* @param string[] $throughClasses
|
||||||
|
*/
|
||||||
|
private function addRelatedManyManys(array &$classIDs, DataObject $record, array &$throughClasses): void
|
||||||
|
{
|
||||||
|
$class = get_class($record);
|
||||||
|
foreach ($record->manyMany() as $component => $componentClass) {
|
||||||
|
$componentClass = $this->updateComponentClass($componentClass, $throughClasses);
|
||||||
|
if (
|
||||||
|
// Ignore belongs_many_many_through with dot syntax
|
||||||
|
strpos($componentClass, '.') !== false ||
|
||||||
|
// Prevent duplicate counting of self-referential relations e.g.
|
||||||
|
// MyFile::$many_many = [ 'MyFile' => MyFile::class ]
|
||||||
|
// This relation will still be counted in $this::addRelatedReverseManyManys()
|
||||||
|
$record instanceof $componentClass
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$results = $this->fetchManyManyResults($record, $class, $component, false);
|
||||||
|
$this->addResultsToClassIDs($classIDs, $results, $componentClass);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query the database to retrieve many-many results
|
||||||
|
*
|
||||||
|
* @param DataObject $record - The DataObject whose usage data is being retrieved, usually a File
|
||||||
|
* @param string $class - example: My\App\SomePageType
|
||||||
|
* @param string $component - example: 'SomeFiles' - My\App\SomePageType::SomeFiles()
|
||||||
|
* @param bool $reverse - true: SomePage::SomeFiles(), false: SomeFile::SomePages()
|
||||||
|
* @return Query|null
|
||||||
|
*/
|
||||||
|
private function fetchManyManyResults(
|
||||||
|
DataObject $record,
|
||||||
|
string $class,
|
||||||
|
string $component,
|
||||||
|
bool $reverse
|
||||||
|
): ?Query {
|
||||||
|
// Example php file: class MyPage ... private static $many_many = [ 'MyFile' => File::class ]
|
||||||
|
$data = $this->dataObjectSchema->manyManyComponent($class, $component);
|
||||||
|
if (!$data || !($data['join'] ?? false)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$joinTableName = $this->deriveJoinTableName($data);
|
||||||
|
if (!ClassInfo::hasTable($joinTableName)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$usesThroughTable = $data['join'] != $joinTableName;
|
||||||
|
|
||||||
|
$parentField = preg_replace('#ID$#', '', $data['parentField']) . 'ID';
|
||||||
|
$childField = preg_replace('#ID$#', '', $data['childField']) . 'ID';
|
||||||
|
$selectField = !$reverse ? $childField : $parentField;
|
||||||
|
$selectFields = [$selectField];
|
||||||
|
$whereField = !$reverse ? $parentField : $childField;
|
||||||
|
|
||||||
|
// Support for polymorphic through objects such FileLink that allow for multiple class types on one side e.g.
|
||||||
|
// ParentID: int, ParentClass: enum('File::class, SiteTree::class, ElementContent::class, ...')
|
||||||
|
if ($usesThroughTable) {
|
||||||
|
$dbFields = $this->dataObjectSchema->databaseFields($data['join']);
|
||||||
|
if ($parentField === 'ParentID' && isset($dbFields['ParentClass'])) {
|
||||||
|
$selectFields[] = 'ParentClass';
|
||||||
|
if (!$reverse) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent duplicate queries which can happen when an Image is inserted on a Page subclass via TinyMCE
|
||||||
|
// and FileLink will make the same query multiple times for all the different page subclasses because
|
||||||
|
// the FileLink is associated with the Base Page class database table
|
||||||
|
$queryIden = implode('-', array_merge($selectFields, [$joinTableName, $whereField, $record->ID]));
|
||||||
|
if (array_key_exists($queryIden, $this->queryIdens)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$this->queryIdens[$queryIden] = true;
|
||||||
|
|
||||||
|
return SQLSelect::create(
|
||||||
|
sprintf('"' . implode('", "', $selectFields) . '"'),
|
||||||
|
sprintf('"%s"', $joinTableName),
|
||||||
|
sprintf('"%s" = %u', $whereField, $record->ID)
|
||||||
|
)->execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contains special logic for some many_many_through relationships
|
||||||
|
* $joinTableName, instead of the name of the join table, it will be a namespaced classname
|
||||||
|
* Example $class: SilverStripe\Assets\Shortcodes\FileLinkTracking
|
||||||
|
* Example $joinTableName: SilverStripe\Assets\Shortcodes\FileLink
|
||||||
|
*
|
||||||
|
* @param array $data
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private function deriveJoinTableName(array $data): string
|
||||||
|
{
|
||||||
|
$joinTableName = $data['join'];
|
||||||
|
if (!ClassInfo::hasTable($joinTableName) && class_exists($joinTableName)) {
|
||||||
|
$class = $joinTableName;
|
||||||
|
if (!isset($this->classToTableName[$class])) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$joinTableName = $this->classToTableName[$class];
|
||||||
|
}
|
||||||
|
return $joinTableName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array $classIDs
|
||||||
|
* @param DataObject $record
|
||||||
|
* @param string $class
|
||||||
|
*/
|
||||||
|
private function addRelatedReverseHasOnes(array &$classIDs, DataObject $record, string $class): void
|
||||||
|
{
|
||||||
|
foreach (singleton($class)->hasOne() as $component => $componentClass) {
|
||||||
|
if (!($record instanceof $componentClass)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$results = $this->fetchReverseHasOneResults($record, $class, $component);
|
||||||
|
$this->addResultsToClassIDs($classIDs, $results, $class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query the database to retrieve has_one results
|
||||||
|
*
|
||||||
|
* @param DataObject $record - The DataObject whose usage data is being retrieved, usually a File
|
||||||
|
* @param string $class - Name of class with the relation to record
|
||||||
|
* @param string $component - Name of relation to `$record` on `$class`
|
||||||
|
* @return Query|null
|
||||||
|
*/
|
||||||
|
private function fetchReverseHasOneResults(DataObject $record, string $class, string $component): ?Query
|
||||||
|
{
|
||||||
|
// Ensure table exists, this is required for TestOnly SapphireTest classes
|
||||||
|
if (!isset($this->classToTableName[$class])) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$componentIDField = "{$component}ID";
|
||||||
|
|
||||||
|
// Only get database fields from the current class model, not parent class model
|
||||||
|
$dbFields = $this->dataObjectSchema->databaseFields($class, false);
|
||||||
|
if (!isset($dbFields[$componentIDField])) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$tableName = $this->dataObjectSchema->tableName($class);
|
||||||
|
$where = sprintf('"%s" = %u', $componentIDField, $record->ID);
|
||||||
|
|
||||||
|
// Polymorphic
|
||||||
|
if ($componentIDField === 'ParentID' && isset($dbFields['ParentClass'])) {
|
||||||
|
$where .= sprintf(' AND "ParentClass" = %s', $this->prepareClassNameLiteral(get_class($record)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example SQL:
|
||||||
|
// Normal:
|
||||||
|
// SELECT "ID" FROM "MyPage" WHERE "MyFileID" = 123;
|
||||||
|
// Polymorphic:
|
||||||
|
// SELECT "ID" FROM "MyPage" WHERE "ParentID" = 456 AND "ParentClass" = 'MyFile';
|
||||||
|
return SQLSelect::create(
|
||||||
|
'"ID"',
|
||||||
|
sprintf('"%s"', $tableName),
|
||||||
|
$where
|
||||||
|
)->execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array $classIDs
|
||||||
|
* @param DataObject $record
|
||||||
|
* @param string $class
|
||||||
|
* @param string[] $throughClasses
|
||||||
|
*/
|
||||||
|
private function addRelatedReverseManyManys(
|
||||||
|
array &$classIDs,
|
||||||
|
DataObject $record,
|
||||||
|
string $class,
|
||||||
|
array &$throughClasses
|
||||||
|
): void {
|
||||||
|
foreach (singleton($class)->manyMany() as $component => $componentClass) {
|
||||||
|
$componentClass = $this->updateComponentClass($componentClass, $throughClasses);
|
||||||
|
if (!($record instanceof $componentClass) ||
|
||||||
|
// Ignore belongs_many_many_through with dot syntax
|
||||||
|
strpos($componentClass, '.') !== false
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$results = $this->fetchManyManyResults($record, $class, $component, true);
|
||||||
|
$this->addResultsToClassIDs($classIDs, $results, $class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the `$classIDs` array with the relationship IDs from database `$results`
|
||||||
|
*
|
||||||
|
* @param array $classIDs
|
||||||
|
* @param Query|null $results
|
||||||
|
* @param string $class
|
||||||
|
*/
|
||||||
|
private function addResultsToClassIDs(array &$classIDs, ?Query $results, string $class): void
|
||||||
|
{
|
||||||
|
if (is_null($results) || (!is_subclass_of($class, DataObject::class) && $class !== DataObject::class)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
foreach ($results as $row) {
|
||||||
|
if (count(array_keys($row)) === 2 && isset($row['ParentClass']) && isset($row['ParentID'])) {
|
||||||
|
// Example $class: SilverStripe\Assets\Shortcodes\FileLinkTracking
|
||||||
|
// Example $parentClass: Page
|
||||||
|
$parentClass = $row['ParentClass'];
|
||||||
|
$classIDs[$parentClass] = $classIDs[$parentClass] ?? [];
|
||||||
|
$classIDs[$parentClass][] = $row['ParentID'];
|
||||||
|
} else {
|
||||||
|
if ($class === DataObject::class) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
foreach (array_values($row) as $classID) {
|
||||||
|
$classIDs[$class] = $classIDs[$class] ?? [];
|
||||||
|
$classIDs[$class][] = $classID;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare an FQCN literal for database querying so that backslashes are escaped properly
|
||||||
|
*
|
||||||
|
* @param string $value
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private function prepareClassNameLiteral(string $value): string
|
||||||
|
{
|
||||||
|
$c = chr(92);
|
||||||
|
$escaped = str_replace($c, "{$c}{$c}", $value);
|
||||||
|
// postgres
|
||||||
|
if (stripos(get_class(DB::get_conn()), 'postgres') !== false) {
|
||||||
|
return "E'{$escaped}'";
|
||||||
|
}
|
||||||
|
// mysql
|
||||||
|
return "'{$escaped}'";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a many_many_through $componentClass array to the 'to' component on the 'through' object
|
||||||
|
* If $componentClass represents a through object, then also update the $throughClasses array
|
||||||
|
*
|
||||||
|
* @param string|array $componentClass
|
||||||
|
* @param string[] $throughClasses
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private function updateComponentClass($componentClass, array &$throughClasses): string
|
||||||
|
{
|
||||||
|
if (!is_array($componentClass)) {
|
||||||
|
return $componentClass;
|
||||||
|
}
|
||||||
|
$throughClass = $componentClass['through'];
|
||||||
|
$throughClasses[$throughClass] = true;
|
||||||
|
$lowercaseThroughClass = strtolower($throughClass);
|
||||||
|
$toComponent = $componentClass['to'];
|
||||||
|
return $this->config[$lowercaseThroughClass]['has_one'][$toComponent];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup function to fix unit test specific issue
|
||||||
|
*/
|
||||||
|
private function initClassToTableName(): void
|
||||||
|
{
|
||||||
|
$this->classToTableName = $this->dataObjectSchema->getTableNames();
|
||||||
|
|
||||||
|
// Fix issue that only happens when unit-testing via SapphireTest
|
||||||
|
// TestOnly class tables are only created if they're defined in SapphireTest::$extra_dataobject
|
||||||
|
// This means there's a large number of TestOnly classes, unrelated to the UsedOnTable, that
|
||||||
|
// do not have tables. Remove these table-less classes from $classToTableName.
|
||||||
|
foreach ($this->classToTableName as $class => $tableName) {
|
||||||
|
if (!ClassInfo::hasTable($tableName)) {
|
||||||
|
unset($this->classToTableName[$class]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove classes excluded via Extensions
|
||||||
|
* Remove "through" classes used in many-many relationships
|
||||||
|
*
|
||||||
|
* @param array $classIDs
|
||||||
|
* @param string[] $excludedClasses
|
||||||
|
* @param string[] $throughClasses
|
||||||
|
*/
|
||||||
|
private function removeClasses(array &$classIDs, array $excludedClasses, array $throughClasses): void
|
||||||
|
{
|
||||||
|
foreach (array_keys($classIDs) as $class) {
|
||||||
|
if (isset($throughClasses[$class]) || in_array($class, $excludedClasses)) {
|
||||||
|
unset($classIDs[$class]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch all objects of a class in a single query for better performance
|
||||||
|
*
|
||||||
|
* @param array $classIDs
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private function fetchClassObjs(array $classIDs): array
|
||||||
|
{
|
||||||
|
/** @var DataObject $class */
|
||||||
|
$classObjs = [];
|
||||||
|
foreach ($classIDs as $class => $ids) {
|
||||||
|
$classObjs[$class] = [];
|
||||||
|
foreach ($class::get()->filter('ID', $ids) as $obj) {
|
||||||
|
$classObjs[$class][$obj->ID] = $obj;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $classObjs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returned ArrayList can have multiple entries for the same DataObject
|
||||||
|
* For example, the File is used multiple times on a single Page
|
||||||
|
*
|
||||||
|
* @param array $classIDs
|
||||||
|
* @param array $classObjs
|
||||||
|
* @return ArrayList
|
||||||
|
*/
|
||||||
|
private function deriveList(array $classIDs, array $classObjs): ArrayList
|
||||||
|
{
|
||||||
|
$list = ArrayList::create();
|
||||||
|
foreach ($classIDs as $class => $ids) {
|
||||||
|
foreach ($ids as $id) {
|
||||||
|
// Ensure the $classObj exists, this is to cover an edge case where there is an orphaned
|
||||||
|
// many-many join table database record with no corresponding DataObject database record
|
||||||
|
if (!isset($classObjs[$class][$id])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$list->push($classObjs[$class][$id]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $list;
|
||||||
|
}
|
||||||
|
}
|
376
tests/php/ORM/RelatedDataServiceTest.php
Normal file
376
tests/php/ORM/RelatedDataServiceTest.php
Normal file
@ -0,0 +1,376 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\ORM\Tests;
|
||||||
|
|
||||||
|
use SilverStripe\ORM\SS_List;
|
||||||
|
use SilverStripe\ORM\Tests\RelatedDataServiceTest\Base;
|
||||||
|
use SilverStripe\ORM\Tests\RelatedDataServiceTest\Belongs;
|
||||||
|
use SilverStripe\ORM\Tests\RelatedDataServiceTest\HasMany;
|
||||||
|
use SilverStripe\ORM\Tests\RelatedDataServiceTest\Hub;
|
||||||
|
use SilverStripe\ORM\Tests\RelatedDataServiceTest\HubExtension;
|
||||||
|
use SilverStripe\ORM\Tests\RelatedDataServiceTest\HubSub;
|
||||||
|
use SilverStripe\ORM\Tests\RelatedDataServiceTest\ManyMany;
|
||||||
|
use SilverStripe\ORM\Tests\RelatedDataServiceTest\ManyManyNoBelongs;
|
||||||
|
use SilverStripe\ORM\Tests\RelatedDataServiceTest\ManyManyThrough;
|
||||||
|
use SilverStripe\ORM\Tests\RelatedDataServiceTest\ManyManyThroughNoBelongs;
|
||||||
|
use SilverStripe\ORM\Tests\RelatedDataServiceTest\Node;
|
||||||
|
use SilverStripe\ORM\Tests\RelatedDataServiceTest\Polymorphic;
|
||||||
|
use SilverStripe\ORM\Tests\RelatedDataServiceTest\SelfReferentialNode;
|
||||||
|
use SilverStripe\ORM\Tests\RelatedDataServiceTest\ThroughObject;
|
||||||
|
use SilverStripe\ORM\Tests\RelatedDataServiceTest\ThroughObjectPolymorphic;
|
||||||
|
use SilverStripe\ORM\Tests\RelatedDataServiceTest\ThroughObjectMMT;
|
||||||
|
use SilverStripe\ORM\Tests\RelatedDataServiceTest\ThroughObjectMMTNB;
|
||||||
|
use SilverStripe\Dev\SapphireTest;
|
||||||
|
use SilverStripe\ORM\DataObject;
|
||||||
|
use SilverStripe\ORM\Queries\SQLDelete;
|
||||||
|
|
||||||
|
class RelatedDataServiceTest extends SapphireTest
|
||||||
|
{
|
||||||
|
|
||||||
|
protected $usesDatabase = true;
|
||||||
|
|
||||||
|
// This static is required to get Config to populate
|
||||||
|
// Config is looped within RelatedDataService::findAll()
|
||||||
|
protected static $extra_data_objects = [
|
||||||
|
Base::class,
|
||||||
|
Belongs::class,
|
||||||
|
HasMany::class,
|
||||||
|
Hub::class,
|
||||||
|
HubSub::class,
|
||||||
|
ManyMany::class,
|
||||||
|
ManyManyNoBelongs::class,
|
||||||
|
ManyManyThrough::class,
|
||||||
|
ManyManyThroughNoBelongs::class,
|
||||||
|
Node::class,
|
||||||
|
Polymorphic::class,
|
||||||
|
SelfReferentialNode::class,
|
||||||
|
ThroughObject::class,
|
||||||
|
ThroughObjectMMT::class,
|
||||||
|
ThroughObjectMMTNB::class,
|
||||||
|
ThroughObjectPolymorphic::class,
|
||||||
|
];
|
||||||
|
|
||||||
|
// This is static is required to get the database tables to get created
|
||||||
|
protected static $extra_dataobjects = [
|
||||||
|
Base::class,
|
||||||
|
Belongs::class,
|
||||||
|
HasMany::class,
|
||||||
|
Hub::class,
|
||||||
|
HubSub::class,
|
||||||
|
ManyMany::class,
|
||||||
|
ManyManyNoBelongs::class,
|
||||||
|
ManyManyThrough::class,
|
||||||
|
ManyManyThroughNoBelongs::class,
|
||||||
|
Node::class,
|
||||||
|
Polymorphic::class,
|
||||||
|
SelfReferentialNode::class,
|
||||||
|
ThroughObject::class,
|
||||||
|
ThroughObjectMMT::class,
|
||||||
|
ThroughObjectMMTNB::class,
|
||||||
|
ThroughObjectPolymorphic::class,
|
||||||
|
];
|
||||||
|
|
||||||
|
public function testUnsaved()
|
||||||
|
{
|
||||||
|
$myFile = new Node();
|
||||||
|
// don't write()
|
||||||
|
$list = $myFile->findAllRelatedData();
|
||||||
|
$this->assertTrue($list instanceof SS_List);
|
||||||
|
$this->assertSame(0, $list->count());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUsageUnrelated()
|
||||||
|
{
|
||||||
|
$myFile = new Node();
|
||||||
|
$myFile->write();
|
||||||
|
$myPage = new Hub();
|
||||||
|
$myPage->Title = 'Unrelated page';
|
||||||
|
$myPage->write();
|
||||||
|
$list = $myFile->findAllRelatedData();
|
||||||
|
$this->assertSame(0, $list->count());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUsageHasOne()
|
||||||
|
{
|
||||||
|
$pageTitle = 'My Page that has_one File';
|
||||||
|
$myFile = new Node();
|
||||||
|
$myFile->write();
|
||||||
|
$myPage = new Hub();
|
||||||
|
$myPage->Title = $pageTitle;
|
||||||
|
$myPage->HO = $myFile;
|
||||||
|
$myPage->write();
|
||||||
|
$list = $myFile->findAllRelatedData();
|
||||||
|
$this->assertSame(1, $list->count());
|
||||||
|
$this->assertSame($pageTitle, $list->first()->Title);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUsageHasOneHubExtension()
|
||||||
|
{
|
||||||
|
// Add DataExtension and reset database so that tables + columns get added
|
||||||
|
Hub::add_extension(HubExtension::class);
|
||||||
|
DataObject::reset();
|
||||||
|
self::resetDBSchema(true, true);
|
||||||
|
//
|
||||||
|
$pageTitle = 'My Page that has_one File using HubExtension';
|
||||||
|
$myFile = new Node();
|
||||||
|
$myFile->write();
|
||||||
|
$myPage = new Hub();
|
||||||
|
$myPage->Title = $pageTitle;
|
||||||
|
$myPage->ExtHO = $myFile;
|
||||||
|
$myPage->write();
|
||||||
|
$list = $myFile->findAllRelatedData();
|
||||||
|
$this->assertSame(1, $list->count());
|
||||||
|
$this->assertSame($pageTitle, $list->first()->Title);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUsageHubSub()
|
||||||
|
{
|
||||||
|
$pageTitle = 'My Sub Page';
|
||||||
|
$pageSubTitle = 'My SubTitle';
|
||||||
|
$myFile = new Node();
|
||||||
|
$myFile->write();
|
||||||
|
$myPage = new HubSub();
|
||||||
|
$myPage->Title = $pageTitle;
|
||||||
|
$myPage->SubTitle = $pageSubTitle;
|
||||||
|
$myPage->HO = $myFile;
|
||||||
|
$myPage->write();
|
||||||
|
$list = $myFile->findAllRelatedData();
|
||||||
|
$this->assertSame(1, $list->count());
|
||||||
|
$this->assertSame($pageTitle, $list->first()->Title);
|
||||||
|
$this->assertSame($pageSubTitle, $list->first()->SubTitle);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUsageHasOnePolymorphic()
|
||||||
|
{
|
||||||
|
$pageTitle = 'My Page that has_one File polymorphic';
|
||||||
|
$myFile = new Node();
|
||||||
|
$myFile->write();
|
||||||
|
$myPage = new Hub();
|
||||||
|
$myPage->Title = $pageTitle;
|
||||||
|
$myPage->Parent = $myFile;
|
||||||
|
$myPage->write();
|
||||||
|
$list = $myFile->findAllRelatedData();
|
||||||
|
$this->assertSame(1, $list->count());
|
||||||
|
$this->assertSame($pageTitle, $list->first()->Title);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUsageHasOnePolymorphicOnNode()
|
||||||
|
{
|
||||||
|
$pageTitle = 'My Page that that belongs to a polymorphic File';
|
||||||
|
$myPage = new Hub();
|
||||||
|
$myPage->Title = $pageTitle;
|
||||||
|
$myPage->write();
|
||||||
|
$myFile = new Polymorphic();
|
||||||
|
$myFile->Parent = $myPage;
|
||||||
|
$myFile->write();
|
||||||
|
$list = $myFile->findAllRelatedData();
|
||||||
|
$this->assertSame(1, $list->count());
|
||||||
|
$this->assertSame($pageTitle, $list->first()->Title);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUsageHasMany()
|
||||||
|
{
|
||||||
|
$pageTitle = 'My Page that has_many File';
|
||||||
|
$myPage = new Hub();
|
||||||
|
$myPage->Title = $pageTitle;
|
||||||
|
$myPage->write();
|
||||||
|
$myFile = new HasMany();
|
||||||
|
$myFile->write();
|
||||||
|
$myPage->HM()->add($myFile);
|
||||||
|
$list = $myFile->findAllRelatedData();
|
||||||
|
$this->assertSame(1, $list->count());
|
||||||
|
$this->assertSame($pageTitle, $list->first()->Title);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUsageManyManyWithBelongs()
|
||||||
|
{
|
||||||
|
$pageTitle = 'My Page that many_many File with belong_many_many Page';
|
||||||
|
$myPage = new Hub();
|
||||||
|
$myPage->Title = $pageTitle;
|
||||||
|
$myPage->write();
|
||||||
|
$myFile = new Belongs();
|
||||||
|
$myFile->write();
|
||||||
|
$myPage->MMtoBMM()->add($myFile);
|
||||||
|
$list = $myFile->findAllRelatedData();
|
||||||
|
$this->assertSame(1, $list->count());
|
||||||
|
$this->assertSame($pageTitle, $list->first()->Title);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUsageManyManyWithoutBelongs()
|
||||||
|
{
|
||||||
|
$pageTitle = 'My Page that many_many File without belong_many_many Page';
|
||||||
|
$myPage = new Hub();
|
||||||
|
$myPage->Title = $pageTitle;
|
||||||
|
$myPage->write();
|
||||||
|
$myFile = new Node();
|
||||||
|
$myFile->write();
|
||||||
|
$myPage->MMtoNoBMM()->add($myFile);
|
||||||
|
$list = $myFile->findAllRelatedData();
|
||||||
|
$this->assertSame(1, $list->count());
|
||||||
|
$this->assertSame($pageTitle, $list->first()->Title);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUsageManyManyWithoutBelongsHubExtension()
|
||||||
|
{
|
||||||
|
// Add DataExtension and reset database so that tables + columns get added
|
||||||
|
Hub::add_extension(HubExtension::class);
|
||||||
|
DataObject::reset();
|
||||||
|
self::resetDBSchema(true, true);
|
||||||
|
//
|
||||||
|
$pageTitle = 'My Page that many_many File without belong_many_many Page using HubExtension';
|
||||||
|
$myPage = new Hub();
|
||||||
|
$myPage->Title = $pageTitle;
|
||||||
|
$myPage->write();
|
||||||
|
$myFile = new Node();
|
||||||
|
$myFile->write();
|
||||||
|
$myPage->ExtMMtoNoBMM()->add($myFile);
|
||||||
|
$list = $myFile->findAllRelatedData();
|
||||||
|
$this->assertSame(1, $list->count());
|
||||||
|
$this->assertSame($pageTitle, $list->first()->Title);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUsageManyManyWithoutBelongsOrphanedJoinTable()
|
||||||
|
{
|
||||||
|
$pageTitle = 'My Page that many_many File without belong_many_many Page orphaned join table';
|
||||||
|
$myPage = new Hub();
|
||||||
|
$myPage->Title = $pageTitle;
|
||||||
|
$myPage->write();
|
||||||
|
$myFile = new Node();
|
||||||
|
$myFile->write();
|
||||||
|
$myPage->MMtoNoBMM()->add($myFile);
|
||||||
|
// manually delete Page record from database, leaving join table record intact
|
||||||
|
SQLDelete::create('"TestOnly_RelatedDataServiceTest_Hub"', sprintf('"ID" = %s', $myPage->ID))->execute();
|
||||||
|
SQLDelete::create('"TestOnly_RelatedDataServiceTest_Base"', sprintf('"ID" = %s', $myPage->ID))->execute();
|
||||||
|
$list = $myFile->findAllRelatedData();
|
||||||
|
$this->assertSame(0, $list->count());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUsageBelongsManyMany()
|
||||||
|
{
|
||||||
|
$pageTitle = 'My Page that belongs_many_many File with many_many Page';
|
||||||
|
$pageTitle2 = 'My other Page that belongs_many_many File with many_many Page';
|
||||||
|
$myPage = new Hub();
|
||||||
|
$myPage->Title = $pageTitle;
|
||||||
|
$myPage->write();
|
||||||
|
$myPage2 = new Hub();
|
||||||
|
$myPage2->Title = $pageTitle2;
|
||||||
|
$myPage2->write();
|
||||||
|
$myFile = new ManyMany();
|
||||||
|
$myFile->write();
|
||||||
|
// add from both pages from different directions
|
||||||
|
$myPage->BMMtoMM()->add($myFile);
|
||||||
|
$myFile->Hubs()->add($myPage2);
|
||||||
|
$list = $myFile->findAllRelatedData();
|
||||||
|
$this->assertSame(2, $list->count());
|
||||||
|
$this->assertSame($pageTitle, $list->first()->Title);
|
||||||
|
$this->assertSame($pageTitle2, $list->last()->Title);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUsageManyManyThrough()
|
||||||
|
{
|
||||||
|
$pageTitle = 'My Page that many_many_through File';
|
||||||
|
$myPage = new Hub();
|
||||||
|
$myPage->Title = $pageTitle;
|
||||||
|
$myPage->write();
|
||||||
|
$myFile = new Node();
|
||||||
|
$myFile->write();
|
||||||
|
$myPage->MMT()->add($myFile);
|
||||||
|
$list = $myFile->findAllRelatedData();
|
||||||
|
$this->assertSame(1, $list->count());
|
||||||
|
$this->assertSame($pageTitle, $list->first()->Title);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUsageManyManyThroughPolymorphic()
|
||||||
|
{
|
||||||
|
$pageTitle = 'My Page that many_many_through_parent_class File';
|
||||||
|
$myPage = new Hub();
|
||||||
|
$myPage->Title = $pageTitle;
|
||||||
|
$myPage->write();
|
||||||
|
$myFile = new Node();
|
||||||
|
$myFile->write();
|
||||||
|
$myPage->MMTP()->add($myFile);
|
||||||
|
$list = $myFile->findAllRelatedData();
|
||||||
|
$this->assertSame(1, $list->count());
|
||||||
|
$this->assertSame($pageTitle, $list->first()->Title);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUsageFileManyManyWithoutPageBelongs()
|
||||||
|
{
|
||||||
|
$pageTitle = 'My Page that not belongs_many_many File with many_many Page';
|
||||||
|
$myPage = new Hub();
|
||||||
|
$myPage->Title = $pageTitle;
|
||||||
|
$myPage->write();
|
||||||
|
$myFile = new ManyManyNoBelongs();
|
||||||
|
$myFile->write();
|
||||||
|
$myFile->Hubs()->add($myPage);
|
||||||
|
$list = $myFile->findAllRelatedData();
|
||||||
|
$this->assertSame(1, $list->count());
|
||||||
|
$this->assertSame($pageTitle, $list->first()->Title);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUsageFileManyManyThroughWithPageBelongs()
|
||||||
|
{
|
||||||
|
$pageTitle = 'My Page that many_many_belongs File with many_many_through Page';
|
||||||
|
$pageTitle2 = 'My other Page that many_many_belongs File with many_many_through Page';
|
||||||
|
$myPage = new Hub();
|
||||||
|
$myPage->Title = $pageTitle;
|
||||||
|
$myPage->write();
|
||||||
|
$myPage2 = new Hub();
|
||||||
|
$myPage2->Title = $pageTitle2;
|
||||||
|
$myPage2->write();
|
||||||
|
$myFile = new ManyManyThrough();
|
||||||
|
$myFile->write();
|
||||||
|
// add from both pages from different directions
|
||||||
|
$myPage->BMMtoMMT()->add($myFile);
|
||||||
|
$myFile->Hubs()->add($myPage2);
|
||||||
|
$list = $myFile->findAllRelatedData();
|
||||||
|
$this->assertSame(2, $list->count());
|
||||||
|
$this->assertSame($pageTitle, $list->first()->Title);
|
||||||
|
$this->assertSame($pageTitle2, $list->last()->Title);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUsageFileManyManyThroughWithoutPageBelongs()
|
||||||
|
{
|
||||||
|
$pageTitle = 'My Page that does not many_many_belongs File that many_many_through Page';
|
||||||
|
$myPage = new Hub();
|
||||||
|
$myPage->Title = $pageTitle;
|
||||||
|
$myPage->write();
|
||||||
|
$myFile = new ManyManyThroughNoBelongs();
|
||||||
|
$myFile->write();
|
||||||
|
$myFile->Hubs()->add($myPage);
|
||||||
|
$list = $myFile->findAllRelatedData();
|
||||||
|
$this->assertSame(1, $list->count());
|
||||||
|
$this->assertSame($pageTitle, $list->first()->Title);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSelfReferentialHasOne()
|
||||||
|
{
|
||||||
|
$myFile = new SelfReferentialNode();
|
||||||
|
$myFile->Title = 'My self referential file has_one';
|
||||||
|
$myFile->write();
|
||||||
|
$myFile->HOA = $myFile;
|
||||||
|
$myFile->HOB = $myFile;
|
||||||
|
$myFile->write();
|
||||||
|
$list = $myFile->findAllRelatedData();
|
||||||
|
$this->assertSame(2, $list->count());
|
||||||
|
$this->assertSame($myFile->Title, $list->first()->Title);
|
||||||
|
$this->assertTrue($list->first() instanceof SelfReferentialNode);
|
||||||
|
$this->assertSame($myFile->Title, $list->last()->Title);
|
||||||
|
$this->assertTrue($list->last() instanceof SelfReferentialNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSelfReferentialManyMany()
|
||||||
|
{
|
||||||
|
$myFile = new SelfReferentialNode();
|
||||||
|
$myFile->Title = 'My self referential file many_many';
|
||||||
|
$myFile->write();
|
||||||
|
$myFile->MMA()->add($myFile);
|
||||||
|
$myFile->MMB()->add($myFile);
|
||||||
|
$list = $myFile->findAllRelatedData();
|
||||||
|
$this->assertSame(2, $list->count());
|
||||||
|
$this->assertSame($myFile->Title, $list->first()->Title);
|
||||||
|
$this->assertTrue($list->first() instanceof SelfReferentialNode);
|
||||||
|
$this->assertSame($myFile->Title, $list->last()->Title);
|
||||||
|
$this->assertTrue($list->last() instanceof SelfReferentialNode);
|
||||||
|
}
|
||||||
|
}
|
15
tests/php/ORM/RelatedDataServiceTest/Base.php
Normal file
15
tests/php/ORM/RelatedDataServiceTest/Base.php
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\ORM\Tests\RelatedDataServiceTest;
|
||||||
|
|
||||||
|
use SilverStripe\Dev\TestOnly;
|
||||||
|
use SilverStripe\ORM\DataObject;
|
||||||
|
|
||||||
|
class Base extends DataObject implements TestOnly
|
||||||
|
{
|
||||||
|
private static $table_name = 'TestOnly_RelatedDataServiceTest_Base';
|
||||||
|
|
||||||
|
private static $db = [
|
||||||
|
'Title' => 'Varchar'
|
||||||
|
];
|
||||||
|
}
|
12
tests/php/ORM/RelatedDataServiceTest/Belongs.php
Normal file
12
tests/php/ORM/RelatedDataServiceTest/Belongs.php
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\ORM\Tests\RelatedDataServiceTest;
|
||||||
|
|
||||||
|
class Belongs extends Node
|
||||||
|
{
|
||||||
|
private static $table_name = 'TestOnly_RelatedDataServiceTest_Belongs';
|
||||||
|
|
||||||
|
private static $belongs_many_many = [
|
||||||
|
'Hubs' => Hub::class
|
||||||
|
];
|
||||||
|
}
|
12
tests/php/ORM/RelatedDataServiceTest/HasMany.php
Normal file
12
tests/php/ORM/RelatedDataServiceTest/HasMany.php
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\ORM\Tests\RelatedDataServiceTest;
|
||||||
|
|
||||||
|
class HasMany extends Node
|
||||||
|
{
|
||||||
|
private static $table_name = 'TestOnly_RelatedDataServiceTest_HasMany';
|
||||||
|
|
||||||
|
private static $has_one = [
|
||||||
|
'Hub' => Hub::class
|
||||||
|
];
|
||||||
|
}
|
52
tests/php/ORM/RelatedDataServiceTest/Hub.php
Normal file
52
tests/php/ORM/RelatedDataServiceTest/Hub.php
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\ORM\Tests\RelatedDataServiceTest;
|
||||||
|
|
||||||
|
// The "Hub" acts similar to how a Page would normally behave
|
||||||
|
// Though it's been kept as a DataObject to keep things more abstract
|
||||||
|
// All the other 'RelatedDataServiceTest_*' classes represent Files (excluding _ExtText_*)
|
||||||
|
use SilverStripe\Assets\Shortcodes\FileLink;
|
||||||
|
use SilverStripe\ORM\DataObject;
|
||||||
|
|
||||||
|
class Hub extends Base
|
||||||
|
{
|
||||||
|
private static $table_name = 'TestOnly_RelatedDataServiceTest_Hub';
|
||||||
|
|
||||||
|
private static $has_one = [
|
||||||
|
'HO' => Node::class,
|
||||||
|
'Parent' => DataObject::class, // Will create a ParentID column + ParentColumn Enum column
|
||||||
|
];
|
||||||
|
|
||||||
|
private static $has_many = [
|
||||||
|
'HM' => HasMany::class
|
||||||
|
];
|
||||||
|
|
||||||
|
private static $many_many = [
|
||||||
|
// has belongs_many_many on the other end
|
||||||
|
'MMtoBMM' => Belongs::class,
|
||||||
|
// does not have belong_many_many on the other end
|
||||||
|
'MMtoNoBMM' => Node::class,
|
||||||
|
// manyManyThrough
|
||||||
|
'MMT' => [
|
||||||
|
'through' => ThroughObject::class,
|
||||||
|
'from' => 'HubObj',
|
||||||
|
'to' => 'NodeObj',
|
||||||
|
],
|
||||||
|
// manyManyThrough Polymorphic
|
||||||
|
'MMTP' => [
|
||||||
|
'through' => ThroughObjectPolymorphic::class,
|
||||||
|
'from' => 'Parent',
|
||||||
|
'to' => 'NodeObj',
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
private static $belongs_many_many = [
|
||||||
|
// has many_many on the other end
|
||||||
|
'BMMtoMM' => ManyMany::class,
|
||||||
|
'BMMtoMMT' => ManyManyThrough::class
|
||||||
|
// Not testing the following, will throw this Silverstripe error:
|
||||||
|
// belongs_many_many relation ... points to ... without matching many_many
|
||||||
|
// does not have many_many on the other end
|
||||||
|
// 'BMMtoNoMM' => Node::class
|
||||||
|
];
|
||||||
|
}
|
18
tests/php/ORM/RelatedDataServiceTest/HubExtension.php
Normal file
18
tests/php/ORM/RelatedDataServiceTest/HubExtension.php
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\ORM\Tests\RelatedDataServiceTest;
|
||||||
|
|
||||||
|
use SilverStripe\Dev\TestOnly;
|
||||||
|
use SilverStripe\ORM\DataExtension;
|
||||||
|
|
||||||
|
class HubExtension extends DataExtension implements TestOnly
|
||||||
|
{
|
||||||
|
private static $has_one = [
|
||||||
|
'ExtHO' => Node::class
|
||||||
|
];
|
||||||
|
|
||||||
|
private static $many_many = [
|
||||||
|
// does not have belong_many_many on the other end
|
||||||
|
'ExtMMtoNoBMM' => Node::class
|
||||||
|
];
|
||||||
|
}
|
12
tests/php/ORM/RelatedDataServiceTest/HubSub.php
Normal file
12
tests/php/ORM/RelatedDataServiceTest/HubSub.php
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\ORM\Tests\RelatedDataServiceTest;
|
||||||
|
|
||||||
|
class HubSub extends Hub
|
||||||
|
{
|
||||||
|
private static $table_name = 'TestOnly_RelatedDataServiceTest_HubSub';
|
||||||
|
|
||||||
|
private static $db = [
|
||||||
|
'SubTitle' => 'Varchar'
|
||||||
|
];
|
||||||
|
}
|
12
tests/php/ORM/RelatedDataServiceTest/ManyMany.php
Normal file
12
tests/php/ORM/RelatedDataServiceTest/ManyMany.php
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\ORM\Tests\RelatedDataServiceTest;
|
||||||
|
|
||||||
|
class ManyMany extends Node
|
||||||
|
{
|
||||||
|
private static $table_name = 'TestOnly_RelatedDataServiceTest_ManyMany';
|
||||||
|
|
||||||
|
private static $many_many = [
|
||||||
|
'Hubs' => Hub::class
|
||||||
|
];
|
||||||
|
}
|
13
tests/php/ORM/RelatedDataServiceTest/ManyManyNoBelongs.php
Normal file
13
tests/php/ORM/RelatedDataServiceTest/ManyManyNoBelongs.php
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\ORM\Tests\RelatedDataServiceTest;
|
||||||
|
|
||||||
|
// No belong_many_many on RelatedDataServiceTest_Hub
|
||||||
|
class ManyManyNoBelongs extends Node
|
||||||
|
{
|
||||||
|
private static $table_name = 'TestOnly_RelatedDataServiceTest_ManyManyNoBelongs';
|
||||||
|
|
||||||
|
private static $many_many = [
|
||||||
|
'Hubs' => Hub::class
|
||||||
|
];
|
||||||
|
}
|
16
tests/php/ORM/RelatedDataServiceTest/ManyManyThrough.php
Normal file
16
tests/php/ORM/RelatedDataServiceTest/ManyManyThrough.php
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\ORM\Tests\RelatedDataServiceTest;
|
||||||
|
|
||||||
|
class ManyManyThrough extends Node
|
||||||
|
{
|
||||||
|
private static $table_name = 'TestOnly_RelatedDataServiceTest_ManyManyThrough';
|
||||||
|
|
||||||
|
private static $many_many = [
|
||||||
|
'Hubs' => [
|
||||||
|
'through' => ThroughObjectMMT::class,
|
||||||
|
'from' => 'MMTObj',
|
||||||
|
'to' => 'HubObj',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\ORM\Tests\RelatedDataServiceTest;
|
||||||
|
|
||||||
|
class ManyManyThroughNoBelongs extends Node
|
||||||
|
{
|
||||||
|
private static $table_name = 'TestOnly_RelatedDataServiceTest_ManyManyThroughNoBelongs';
|
||||||
|
|
||||||
|
private static $many_many = [
|
||||||
|
'Hubs' => [
|
||||||
|
'through' => ThroughObjectMMTNB::class,
|
||||||
|
// note: you cannot swap from/to around, Silverstripe MMT expects 'from' to be
|
||||||
|
// the type of class that defines the many_many_through relationship
|
||||||
|
// i.e. this class
|
||||||
|
'from' => 'MMTNBObj',
|
||||||
|
'to' => 'HubObj',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
8
tests/php/ORM/RelatedDataServiceTest/Node.php
Normal file
8
tests/php/ORM/RelatedDataServiceTest/Node.php
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\ORM\Tests\RelatedDataServiceTest;
|
||||||
|
|
||||||
|
class Node extends Base
|
||||||
|
{
|
||||||
|
private static $table_name = 'TestOnly_RelatedDataServiceTest_Node';
|
||||||
|
}
|
14
tests/php/ORM/RelatedDataServiceTest/Polymorphic.php
Normal file
14
tests/php/ORM/RelatedDataServiceTest/Polymorphic.php
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\ORM\Tests\RelatedDataServiceTest;
|
||||||
|
|
||||||
|
use SilverStripe\ORM\DataObject;
|
||||||
|
|
||||||
|
class Polymorphic extends Node
|
||||||
|
{
|
||||||
|
private static $table_name = 'TestOnly_RelatedDataServiceTest_Polymorphic';
|
||||||
|
|
||||||
|
private static $has_one = [
|
||||||
|
'Parent' => DataObject::class
|
||||||
|
];
|
||||||
|
}
|
18
tests/php/ORM/RelatedDataServiceTest/SelfReferentialNode.php
Normal file
18
tests/php/ORM/RelatedDataServiceTest/SelfReferentialNode.php
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\ORM\Tests\RelatedDataServiceTest;
|
||||||
|
|
||||||
|
class SelfReferentialNode extends Node
|
||||||
|
{
|
||||||
|
private static $table_name = 'TestOnly_RelatedDataServiceTest_SelfReferentialNode';
|
||||||
|
|
||||||
|
private static $has_one = [
|
||||||
|
'HOA' => Node::class,
|
||||||
|
'HOB' => SelfReferentialNode::class
|
||||||
|
];
|
||||||
|
|
||||||
|
private static $many_many = [
|
||||||
|
'MMA' => Node::class,
|
||||||
|
'MMB' => SelfReferentialNode::class
|
||||||
|
];
|
||||||
|
}
|
13
tests/php/ORM/RelatedDataServiceTest/ThroughObject.php
Normal file
13
tests/php/ORM/RelatedDataServiceTest/ThroughObject.php
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\ORM\Tests\RelatedDataServiceTest;
|
||||||
|
|
||||||
|
class ThroughObject extends Node
|
||||||
|
{
|
||||||
|
private static $table_name = 'TestOnly_RelatedDataServiceTest_ThroughObject';
|
||||||
|
|
||||||
|
private static $has_one = [
|
||||||
|
'HubObj' => Hub::class,
|
||||||
|
'NodeObj' => Node::class,
|
||||||
|
];
|
||||||
|
}
|
13
tests/php/ORM/RelatedDataServiceTest/ThroughObjectMMT.php
Normal file
13
tests/php/ORM/RelatedDataServiceTest/ThroughObjectMMT.php
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\ORM\Tests\RelatedDataServiceTest;
|
||||||
|
|
||||||
|
class ThroughObjectMMT extends Node
|
||||||
|
{
|
||||||
|
private static $table_name = 'TestOnly_RelatedDataServiceTest_ThroughObjectMMT';
|
||||||
|
|
||||||
|
private static $has_one = [
|
||||||
|
'HubObj' => Hub::class,
|
||||||
|
'MMTObj' => ManyManyThrough::class,
|
||||||
|
];
|
||||||
|
}
|
13
tests/php/ORM/RelatedDataServiceTest/ThroughObjectMMTNB.php
Normal file
13
tests/php/ORM/RelatedDataServiceTest/ThroughObjectMMTNB.php
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\ORM\Tests\RelatedDataServiceTest;
|
||||||
|
|
||||||
|
class ThroughObjectMMTNB extends Node
|
||||||
|
{
|
||||||
|
private static $table_name = 'TestOnly_RelatedDataServiceTest_ThroughObjectMMTNB';
|
||||||
|
|
||||||
|
private static $has_one = [
|
||||||
|
'HubObj' => Hub::class,
|
||||||
|
'MMTNBObj' => ManyManyThroughNoBelongs::class,
|
||||||
|
];
|
||||||
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\ORM\Tests\RelatedDataServiceTest;
|
||||||
|
|
||||||
|
use SilverStripe\ORM\DataObject;
|
||||||
|
|
||||||
|
class ThroughObjectPolymorphic extends Node
|
||||||
|
{
|
||||||
|
private static $table_name = 'TestOnly_RelatedDataServiceTest_ThroughObjectPolymorphic';
|
||||||
|
|
||||||
|
private static $has_one = [
|
||||||
|
'Parent' => DataObject::class, // Will create a ParentID column + ParentColumn Enum column
|
||||||
|
'NodeObj' => Node::class,
|
||||||
|
];
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user