NEW: Static validation for relationships. (#9874)

* NEW: Static validation for relationships.

* Unit tests added.

* PR fixes

* PR feedback: Execute validation on flush.

* PR fixes.

* PR fixes.
This commit is contained in:
Mojmir Fendek 2022-02-04 14:41:09 +13:00 committed by GitHub
parent 8f1c68db42
commit 89c87ddbf8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 1168 additions and 3 deletions

View File

@ -6,4 +6,7 @@ SilverStripe\Security\Member:
- SilverStripe\Security\InheritedPermissionFlusher
SilverStripe\Security\Group:
extensions:
- SilverStripe\Security\InheritedPermissionFlusher
- SilverStripe\Security\InheritedPermissionFlusher
SilverStripe\ORM\DatabaseAdmin:
extensions:
- SilverStripe\Dev\Validation\DatabaseAdminExtension

View File

@ -201,7 +201,9 @@ class Company extends DataObject
```
Multiple `$has_one` relationships are okay if they aren't linking to the same object type. Otherwise, they have to be
named.
named. With that said, naming is recommended in all cases as it makes your code more resilient to change. Adding new relationships is easier when you don't need to review and update existing ones.
You can use `RelationValidationService` for validation of relationships. This tool will point out the relationships which may need a review.
If you're using the default scaffolded form fields with multiple `has_one` relationships, you will end up with a CMS field for each relation. If you don't want these you can remove them by their IDs:
@ -224,6 +226,8 @@ declaring the `$belongs_to`.
Similarly with `$has_many`, dot notation can be used to explicitly specify the `$has_one` which refers to this relation.
This is not mandatory unless the relationship would be otherwise ambiguous.
You can use `RelationValidationService` for validation of relationships. This tool will point out the relationships which may need a review.
```php
use SilverStripe\ORM\DataObject;
@ -251,7 +255,15 @@ how the developer wishes to manage this join table.
[warning]
Please specify a $belongs_many_many-relationship on the related class as well, in order
to have the necessary accessors available on both ends.
to have the necessary accessors available on both ends. You can use `RelationValidationService` for validation of relationships. This tool will point out the relationships which may need a review.
Example configuration:
```yaml
SilverStripe\Dev\Validation\RelationValidationService:
output_enabled: true
```
[/warning]
Much like the `has_one` relationship, `many_many` can be navigated through the `ORM` as well.

View File

@ -0,0 +1,34 @@
<?php
namespace SilverStripe\Dev\Validation;
use ReflectionException;
use SilverStripe\Core\Extension;
use SilverStripe\ORM\DatabaseAdmin;
/**
* Hook up static validation to the deb/build process
*
* @method DatabaseAdmin getOwner()
*/
class DatabaseAdminExtension extends Extension
{
/**
* Extension point in @see DatabaseAdmin::doBuild()
*
* @param bool $quiet
* @param bool $populate
* @param bool $testMode
* @throws ReflectionException
*/
public function onAfterBuild(bool $quiet, bool $populate, bool $testMode): void
{
$service = RelationValidationService::singleton();
if (!$service->config()->get('output_enabled')) {
return;
}
$service->executeValidation();
}
}

View File

@ -0,0 +1,635 @@
<?php
namespace SilverStripe\Dev\Validation;
use ReflectionException;
use SilverStripe\Core\ClassInfo;
use SilverStripe\Core\Config\Configurable;
use SilverStripe\Core\Injector\Injectable;
use SilverStripe\Core\Resettable;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\DB;
/**
* Basic validation of relationship setup, this tool makes sure your relationships are set up correctly in both directions
* The validation is configurable and inspection can be narrowed down by namespace, class and relation name
*
* This tool is opt-in and runs via flush and outputs notices
* For strict validation it is recommended to hook this up to your unit test suite
*/
class RelationValidationService implements Resettable
{
use Configurable;
use Injectable;
/**
* Enable / disable validation output during flush
* This is disabled by default (opt-in)
*
* @var bool
*/
private static $output_enabled = false;
/**
* Only inspect classes with the following namespaces/class prefixes
* Empty string is a special value which represents classes without namespaces
* Set the value to null to disable the rule (useful when overriding configuration)
*
* @var array
*/
private static $allow_rules = [
'empty' => '',
'app' => 'App',
];
/**
* Any classes with the following namespaces/class prefixes will not be inspected
* This config is intended to be used together with @see $allow_rules to narrow down the inspected classes
* Empty string is a special value which represents classes without namespaces
* Set the value to null to disable the rule (useful when overriding configuration)
*
* @var array
*/
private static $deny_rules = [];
/**
* Relations listed here will not be inspected
* Format is <class>.<relation>
* for example: Page::class.'.LinkTracking'
*
* @var array
*/
private static $deny_relations = [];
/**
* Ignore any configuration, useful for debugging specific classes
*
* @var bool
*/
protected $ignoreConfig = false;
/**
* @var array
*/
protected $errors = [];
public function clearErrors(): void
{
$this->errors = [];
}
public static function reset(): void
{
$service = self::singleton();
$service->clearErrors();
$service->ignoreConfig = false;
}
/**
* Hook this into your unit tests and assert for empty array like this
*
* $messages = RelationValidationService::singleton()->validateRelations();
* $this->assertEmpty($messages, print_r($messages, true));
*
* @return array
* @throws ReflectionException
*/
public function validateRelations(): array
{
self::reset();
$classes = ClassInfo::subclassesFor(DataObject::class);
return $this->validateClasses($classes);
}
/**
* @throws ReflectionException
*/
public function executeValidation(): void
{
$errors = $this->validateRelations();
$count = count($errors);
if ($count === 0) {
return;
}
DB::alteration_message(
sprintf(
'%s : %d issues found (listed below)',
ClassInfo::shortName(static::class),
$count
),
'error'
);
foreach ($errors as $message) {
DB::alteration_message($message, 'error');
}
}
/**
* Inspect specified classes - this ignores any configuration
* Useful for checking specific classes when trying to fix relation configuration
*
* @param array $classes
* @return array
*/
public function inspectClasses(array $classes): array
{
self::reset();
$this->ignoreConfig = true;
return $this->validateClasses($classes);
}
/**
* Check if class is ignored during inspection or not
* Useful checking if your configuration works as expected
* Check goes through rules in this order (from generic to more specific):
* 1 - Allow rules
* 2 - Deny rules
* 3 - Deny relations
*
* @param string $class
* @param string|null $relation
* @return bool
*/
public function isIgnored(string $class, ?string $relation = null): bool
{
// Top level override - bail out if configuration should be ignored
if ($this->ignoreConfig) {
return false;
}
// Allow rules - if class doesn't match any allow rule we bail out
if (!$this->matchRules($class, 'allow_rules')) {
return true;
}
// Deny rules - if class matches any deny rule we bail out
if ($this->matchRules($class, 'deny_rules')) {
return true;
}
if ($relation === null) {
// Check is for the class as a whole so we don't need to check specific relation
// Class is considered NOT ignored
return false;
}
// Deny relations
$rules = (array) $this->config()->get('deny_relations');
foreach ($rules as $relationData) {
if ($relationData === null) {
// Disabled rule - bail out
continue;
}
$parsedRelation = $this->parsePlainRelation($relationData);
if ($parsedRelation === null) {
// Invalid rule - bail out
continue;
}
if ($class === $parsedRelation['class'] && $relation === $parsedRelation['relation']) {
// This class and relation combination is supposed to be ignored
return true;
}
}
// Default - Class is considered NOT ignored
return false;
}
/**
* Match class against specified rules
*
* @param string $class
* @param string $rule
* @return bool
*/
protected function matchRules(string $class, string $rule): bool
{
$rules = (array) $this->config()->get($rule);
foreach ($rules as $key => $pattern) {
if ($pattern === null) {
// Disabled rule - bail out
continue;
}
// Special case for classes without a namespace
if ($pattern === '') {
if ($class === ClassInfo::shortName($class)) {
// This is a class without namespace so we match this rule
return true;
}
continue;
}
if (mb_strpos($class, $pattern) === 0) {
// Classname prefix matches the pattern
return true;
}
}
return false;
}
/**
* Execute validation for specified classes
*
* @param array $classes
* @return array
*/
protected function validateClasses(array $classes): array
{
foreach ($classes as $class) {
if ($class === DataObject::class) {
// This is a generic class and doesn't need to be validated
continue;
}
if ($this->isIgnored($class)) {
continue;
}
$this->validateClass($class);
}
return $this->errors;
}
/**
* @param string $class
*/
protected function validateClass(string $class): void
{
if (!is_subclass_of($class, DataObject::class)) {
$this->logError($class, '', 'Inspected class is not a DataObject.');
return;
}
$this->validateHasOne($class);
$this->validateBelongsTo($class);
$this->validateHasMany($class);
$this->validateManyMany($class);
$this->validateBelongsManyMany($class);
}
/**
* @param string $class
*/
protected function validateHasOne(string $class): void
{
$singleton = DataObject::singleton($class);
$relations = (array) $singleton->config()->uninherited('has_one');
foreach ($relations as $relationName => $relationData) {
if ($this->isIgnored($class, $relationName)) {
continue;
}
if (mb_strpos($relationData, '.') !== false) {
$this->logError(
$class,
$relationName,
sprintf('Relation %s is not in the expected format (needs class only format).', $relationData)
);
return;
}
if (!is_subclass_of($relationData, DataObject::class)) {
$this->logError(
$class,
$relationName,
sprintf('Related class %s is not a DataObject.', $relationData)
);
return;
}
$relatedObject = DataObject::singleton($relationData);
// Try to find the back relation - it can be either in belongs_to or has_many
$belongsTo = (array) $relatedObject->config()->uninherited('belongs_to');
$hasMany = (array) $relatedObject->config()->uninherited('has_many');
$found = 0;
foreach ([$hasMany, $belongsTo] as $relationItem) {
foreach ($relationItem as $key => $value) {
$parsedRelation = $this->parsePlainRelation($value);
if ($parsedRelation === null) {
continue;
}
if ($class !== $parsedRelation['class']) {
continue;
}
if ($relationName !== $parsedRelation['relation']) {
continue;
}
$found += 1;
}
}
if ($found === 0) {
$this->logError(
$class,
$relationName,
'Back relation not found or ambiguous (needs class.relation format)'
);
} elseif ($found > 1) {
$this->logError($class, $relationName, 'Back relation is ambiguous');
}
}
}
/**
* @param string $class
*/
protected function validateBelongsTo(string $class): void
{
$singleton = DataObject::singleton($class);
$relations = (array) $singleton->config()->uninherited('belongs_to');
foreach ($relations as $relationName => $relationData) {
if ($this->isIgnored($class, $relationName)) {
continue;
}
$parsedRelation = $this->parsePlainRelation($relationData);
if ($parsedRelation === null) {
$this->logError(
$class,
$relationName,
'Relation is not in the expected format (needs class.relation format)'
);
continue;
}
$relatedClass = $parsedRelation['class'];
$relatedRelation = $parsedRelation['relation'];
if (!is_subclass_of($relatedClass, DataObject::class)) {
$this->logError(
$class,
$relationName,
sprintf('Related class %s is not a DataObject.', $relatedClass)
);
continue;
}
$relatedObject = DataObject::singleton($relatedClass);
$relatedRelations = (array) $relatedObject->config()->uninherited('has_one');
if (array_key_exists($relatedRelation, $relatedRelations)) {
continue;
}
$this->logError($class, $relationName, 'Back relation not found');
}
}
/**
* @param string $class
*/
protected function validateHasMany(string $class): void
{
$singleton = DataObject::singleton($class);
$relations = (array) $singleton->config()->uninherited('has_many');
foreach ($relations as $relationName => $relationData) {
if ($this->isIgnored($class, $relationName)) {
continue;
}
$parsedRelation = $this->parsePlainRelation($relationData);
if ($parsedRelation === null) {
$this->logError(
$class,
$relationName,
'Relation is not in the expected format (needs class.relation format)'
);
continue;
}
$relatedClass = $parsedRelation['class'];
$relatedRelation = $parsedRelation['relation'];
if (!is_subclass_of($relatedClass, DataObject::class)) {
$this->logError(
$class,
$relationName,
sprintf('Related class %s is not a DataObject.', $relatedClass)
);
continue;
}
$relatedObject = DataObject::singleton($relatedClass);
$relatedRelations = (array) $relatedObject->config()->uninherited('has_one');
if (array_key_exists($relatedRelation, $relatedRelations)) {
continue;
}
$this->logError(
$class,
$relationName,
'Back relation not found or ambiguous (needs class.relation format)'
);
}
}
/**
* @param string $class
*/
protected function validateManyMany(string $class): void
{
$singleton = DataObject::singleton($class);
$relations = (array) $singleton->config()->uninherited('many_many');
foreach ($relations as $relationName => $relationData) {
if ($this->isIgnored($class, $relationName)) {
continue;
}
$relatedClass = $this->parseManyManyRelation($relationData);
if (!is_subclass_of($relatedClass, DataObject::class)) {
$this->logError(
$class,
$relationName,
sprintf('Related class %s is not a DataObject.', $relatedClass)
);
continue;
}
$relatedObject = DataObject::singleton($relatedClass);
$relatedRelations = (array) $relatedObject->config()->uninherited('belongs_many_many');
$found = 0;
foreach ($relatedRelations as $key => $value) {
$parsedRelation = $this->parsePlainRelation($value);
if ($parsedRelation === null) {
continue;
}
if ($class !== $parsedRelation['class']) {
continue;
}
if ($relationName !== $parsedRelation['relation']) {
continue;
}
$found += 1;
}
if ($found === 0) {
$this->logError(
$class,
$relationName,
'Back relation not found or ambiguous (needs class.relation format)'
);
} elseif ($found > 1) {
$this->logError($class, $relationName, 'Back relation is ambiguous');
}
}
}
/**
* @param string $class
*/
protected function validateBelongsManyMany(string $class): void
{
$singleton = DataObject::singleton($class);
$relations = (array) $singleton->config()->uninherited('belongs_many_many');
foreach ($relations as $relationName => $relationData) {
if ($this->isIgnored($class, $relationName)) {
continue;
}
$parsedRelation = $this->parsePlainRelation($relationData);
if ($parsedRelation === null) {
$this->logError(
$class,
$relationName,
'Relation is not in the expected format (needs class.relation format)'
);
continue;
}
$relatedClass = $parsedRelation['class'];
$relatedRelation = $parsedRelation['relation'];
if (!is_subclass_of($relatedClass, DataObject::class)) {
$this->logError(
$class,
$relationName,
sprintf('Related class %s is not a DataObject.', $relatedClass)
);
continue;
}
$relatedObject = DataObject::singleton($relatedClass);
$relatedRelations = (array) $relatedObject->config()->uninherited('many_many');
if (array_key_exists($relatedRelation, $relatedRelations)) {
continue;
}
$this->logError($class, $relationName, 'Back relation not found');
}
}
/**
* @param string $relationData
* @return array|null
*/
protected function parsePlainRelation(string $relationData): ?array
{
if (mb_strpos($relationData, '.') === false) {
return null;
}
$segments = explode('.', $relationData);
if (count($segments) !== 2) {
return null;
}
$class = array_shift($segments);
$relation = array_shift($segments);
return [
'class' => $class,
'relation' => $relation,
];
}
/**
* @param array|string $relationData
* @return string|null
*/
protected function parseManyManyRelation($relationData): ?string
{
if (is_array($relationData)) {
foreach (['through', 'to'] as $key) {
if (!array_key_exists($key, $relationData)) {
return null;
}
}
$to = $relationData['to'];
$through = $relationData['through'];
if (!is_subclass_of($through, DataObject::class)) {
return null;
}
$throughObject = DataObject::singleton($through);
$throughRelations = (array) $throughObject->config()->uninherited('has_one');
if (!array_key_exists($to, $throughRelations)) {
return null;
}
return $throughRelations[$to];
}
return $relationData;
}
/**
* @param string $class
* @param string $relation
* @param string $message
*/
protected function logError(string $class, string $relation, string $message)
{
$classPrefix = $relation ? sprintf('%s / %s', $class, $relation) : $class;
$this->errors[] = sprintf('%s : %s', $classPrefix, $message);
}
}

View File

@ -227,6 +227,8 @@ class DatabaseAdmin extends Controller
*/
public function doBuild($quiet = false, $populate = true, $testMode = false)
{
$this->extend('onBeforeBuild', $quiet, $populate, $testMode);
if ($quiet) {
DB::quiet();
} else {
@ -400,6 +402,8 @@ class DatabaseAdmin extends Controller
}
ClassInfo::reset_db_cache();
$this->extend('onAfterBuild', $quiet, $populate, $testMode);
}
/**

View File

@ -0,0 +1,36 @@
<?php
namespace SilverStripe\Dev\Tests\Validation;
use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\DataObject;
/**
* Class Freelancer
*
* @property string $Title
* @method Team TemporaryTeam()
* @method Member TemporaryMember()
*/
class Freelancer extends DataObject implements TestOnly
{
/**
* @var string
*/
private static $table_name = 'RelationValidationTest_Freelancer';
/**
* @var array
*/
private static $db = [
'Title' => 'Varchar(255)',
];
/**
* @var array
*/
private static $has_one = [
'TemporaryTeam' => Team::class,
'TemporaryMember' => Member::class,
];
}

View File

@ -0,0 +1,43 @@
<?php
namespace SilverStripe\Dev\Tests\Validation;
use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\ManyManyList;
/**
* Class Hat
*
* @property string $Title
* @method Member Hatter()
* @method ManyManyList|Team[] TeamHats()
*/
class Hat extends DataObject implements TestOnly
{
/**
* @var string
*/
private static $table_name = 'RelationValidationTest_Hat';
/**
* @var array
*/
private static $db = [
'Title' => 'Varchar(255)',
];
/**
* @var array
*/
private static $belongs_to = [
'Hatter' => Member::class . '.Hat',
];
/**
* @var array
*/
private static $belongs_many_many = [
'TeamHats' => Team::class . '.ReserveHats',
];
}

View File

@ -0,0 +1,54 @@
<?php
namespace SilverStripe\Dev\Tests\Validation;
use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\HasManyList;
use SilverStripe\ORM\ManyManyThroughList;
/**
* Class Member
*
* @property string $Title
* @method Team HomeTeam()
* @method Hat Hat()
* @method HasManyList|Freelancer[] TemporaryMembers()
* @method ManyManyThroughList|Member[] FreelancerTeams()
*/
class Member extends DataObject implements TestOnly
{
/**
* @var string
*/
private static $table_name = 'RelationValidationTest_Member';
/**
* @var array
*/
private static $db = [
'Title' => 'Varchar(255)',
];
/**
* @var array
*/
private static $has_one = [
'HomeTeam' => Team::class,
'Hat' => Hat::class,
];
/**
* @var array
*/
private static $has_many = [
'TemporaryMembers' => Freelancer::class . '.TemporaryMember',
];
/**
* @var array
*/
private static $belongs_many_many = [
'FreelancerTeams' => Team::class . '.Freelancers',
];
}

View File

@ -0,0 +1,291 @@
<?php
namespace SilverStripe\Dev\Tests\Validation;
use Page;
use SilverStripe\Core\Config\Config;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\Dev\Validation\RelationValidationService;
class RelationValidationTest extends SapphireTest
{
/**
* @var array
*/
protected static $extra_dataobjects = [
Team::class,
Member::class,
Hat::class,
Freelancer::class,
];
/**
* @param string|null $class
* @param string|null $field
* @param array $value
* @param array $expected
* @dataProvider validateCasesProvider
*/
public function testValidation(?string $class, ?string $field, array $value, array $expected): void
{
if ($class && $field) {
Config::modify()->set($class, $field, $value);
}
$data = RelationValidationService::singleton()->inspectClasses([
Team::class,
Member::class,
Hat::class,
Freelancer::class,
]);
$this->assertSame($expected, $data);
}
/**
* @param string $class
* @param string|null $relation
* @param array $config
* @param bool $expected
* @dataProvider ignoredClassesProvider
*/
public function testIgnoredClass(string $class, ?string $relation, array $config, bool $expected): void
{
$service = RelationValidationService::singleton();
foreach ($config as $name => $value) {
$service->config()->set($name, $value);
}
$result = $service->isIgnored($class, $relation);
$this->assertEquals($expected, $result);
}
public function validateCasesProvider(): array
{
return [
'correct setup' => [
null,
null,
[],
[],
],
'ambiguous has_one - no relation name' => [
Hat::class,
'belongs_to',
[
'Hatter' => Member::class,
],
[
'SilverStripe\Dev\Tests\Validation\Member / Hat : Back relation not found or ambiguous (needs class.relation format)',
'SilverStripe\Dev\Tests\Validation\Hat / Hatter : Relation is not in the expected format (needs class.relation format)',
],
],
'ambiguous has_one - incorrect relation name' => [
Hat::class,
'belongs_to',
[
'Hatter' => Member::class . '.ObviouslyWrong',
],
[
'SilverStripe\Dev\Tests\Validation\Member / Hat : Back relation not found or ambiguous (needs class.relation format)',
'SilverStripe\Dev\Tests\Validation\Hat / Hatter : Back relation not found',
],
],
'ambiguous has_one - too many relations' => [
Hat::class,
'belongs_to',
[
'Hatter' => Member::class . '.Hat',
'HatterCopy' => Member::class . '.Hat',
],
[
'SilverStripe\Dev\Tests\Validation\Member / Hat : Back relation is ambiguous',
],
],
'invalid has one' => [
Member::class,
'has_one',
[
'HomeTeam' => Team::class . '.UnnecessaryRelation',
'Hat' => Hat::class,
],
[
'SilverStripe\Dev\Tests\Validation\Member / HomeTeam : Relation SilverStripe\Dev\Tests\Validation\Team.UnnecessaryRelation is not in the expected format (needs class only format).'
],
],
'ambiguous has_many - no relation name' => [
Team::class,
'has_many',
[
'Members' => Member::class,
'FreelancerMembers' => Freelancer::class . '.TemporaryTeam',
],
[
'SilverStripe\Dev\Tests\Validation\Team / Members : Relation is not in the expected format (needs class.relation format)',
'SilverStripe\Dev\Tests\Validation\Member / HomeTeam : Back relation not found or ambiguous (needs class.relation format)',
],
],
'ambiguous has_many - incorrect relation name' => [
Team::class,
'has_many',
[
'Members' => Member::class . '.ObviouslyWrong',
'FreelancerMembers' => Freelancer::class . '.TemporaryTeam',
],
[
'SilverStripe\Dev\Tests\Validation\Team / Members : Back relation not found or ambiguous (needs class.relation format)',
'SilverStripe\Dev\Tests\Validation\Member / HomeTeam : Back relation not found or ambiguous (needs class.relation format)',
],
],
'ambiguous has_many - too many relations' => [
Team::class,
'has_many',
[
'Members' => Member::class . '.HomeTeam',
'MembersCopy' => Member::class . '.HomeTeam',
'FreelancerMembers' => Freelancer::class . '.TemporaryTeam',
],
[
'SilverStripe\Dev\Tests\Validation\Member / HomeTeam : Back relation is ambiguous',
],
],
'ambiguous many_many - no relation name' => [
Hat::class,
'belongs_many_many',
[
'TeamHats' => Team::class,
],
[
'SilverStripe\Dev\Tests\Validation\Team / ReserveHats : Back relation not found or ambiguous (needs class.relation format)',
'SilverStripe\Dev\Tests\Validation\Hat / TeamHats : Relation is not in the expected format (needs class.relation format)',
],
],
'ambiguous many_many - incorrect relation name' => [
Hat::class,
'belongs_many_many',
[
'TeamHats' => Team::class . '.ObviouslyWrong',
],
[
'SilverStripe\Dev\Tests\Validation\Team / ReserveHats : Back relation not found or ambiguous (needs class.relation format)',
'SilverStripe\Dev\Tests\Validation\Hat / TeamHats : Back relation not found',
],
],
'ambiguous many_many - too many relations' => [
Hat::class,
'belongs_many_many',
[
'TeamHats' => Team::class . '.ReserveHats',
'TeamHatsCopy' => Team::class . '.ReserveHats',
],
[
'SilverStripe\Dev\Tests\Validation\Team / ReserveHats : Back relation is ambiguous',
],
],
'ambiguous many_many through - no relation name' => [
Member::class,
'belongs_many_many',
[
'FreelancerTeams' => Team::class,
],
[
'SilverStripe\Dev\Tests\Validation\Team / Freelancers : Back relation not found or ambiguous (needs class.relation format)',
'SilverStripe\Dev\Tests\Validation\Member / FreelancerTeams : Relation is not in the expected format (needs class.relation format)',
],
],
'ambiguous many_many through - incorrect relation name' => [
Member::class,
'belongs_many_many',
[
'FreelancerTeams' => Team::class . '.ObviouslyWrong',
],
[
'SilverStripe\Dev\Tests\Validation\Team / Freelancers : Back relation not found or ambiguous (needs class.relation format)',
'SilverStripe\Dev\Tests\Validation\Member / FreelancerTeams : Back relation not found',
],
],
'ambiguous many_many through - too many relations' => [
Member::class,
'belongs_many_many',
[
'FreelancerTeams' => Team::class . '.Freelancers',
'FreelancerTeamsCopy' => Team::class . '.Freelancers',
],
[
'SilverStripe\Dev\Tests\Validation\Team / Freelancers : Back relation is ambiguous',
],
],
];
}
public function ignoredClassesProvider(): array
{
return [
'class default' => [
Team::class,
null,
[],
true,
],
'class relation default' => [
Team::class,
'Members',
[],
true,
],
'page should by included by default (empty namespace)' => [
Page::class,
null,
[],
false,
],
'class relation via allow rules' => [
Team::class,
'Members',
[
'allow_rules' => ['app' => 'SilverStripe\Dev\Tests\Validation'],
],
false,
],
'class included via allow rules but overwritten by deny rules' => [
Team::class,
null,
[
'allow_rules' => ['app' => 'SilverStripe\Dev\Tests\Validation'],
'deny_rules' => [Team::class],
],
true,
],
'class relation included via allow rules but overwritten by deny rules' => [
Team::class,
'Members',
[
'allow_rules' => ['app' => 'SilverStripe\Dev\Tests\Validation'],
'deny_rules' => [Team::class],
],
true,
],
'class relation included via allow rules but overwritten by deny relations' => [
Team::class,
'Members',
[
'allow_rules' => ['app' => 'SilverStripe\Dev\Tests\Validation'],
'deny_relations' => [Team::class . '.Members'],
],
true,
],
'class relation included via allow rules and not overwritten by deny relations of other class' => [
Member::class,
'HomeTeam',
[
'allow_rules' => ['app' => 'SilverStripe\Dev\Tests\Validation'],
'deny_rules' => [Team::class],
'deny_relations' => [Team::class . '.Members'],
],
false,
],
];
}
}

View File

@ -0,0 +1,53 @@
<?php
namespace SilverStripe\Dev\Tests\Validation;
use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\HasManyList;
use SilverStripe\ORM\ManyManyList;
use SilverStripe\ORM\ManyManyThroughList;
/**
* Class Team
*
* @property string $Title
* @method HasManyList|Member[] Members()
* @method HasManyList|Freelancer[] FreelancerMembers()
* @method ManyManyThroughList|Member[] Freelancers()
* @method ManyManyList|Hat[] ReserveHats()
*/
class Team extends DataObject implements TestOnly
{
/**
* @var string
*/
private static $table_name = 'RelationValidationTest_Team';
/**
* @var array
*/
private static $db = [
'Title' => 'Varchar(255)',
];
/**
* @var array
*/
private static $has_many = [
'Members' => Member::class . '.HomeTeam',
'FreelancerMembers' => Freelancer::class . '.TemporaryTeam',
];
/**
* @var array
*/
private static $many_many = [
'ReserveHats' => Hat::class,
'Freelancers' => [
'through' => Freelancer::class,
'from' => 'TemporaryTeam',
'to' => 'TemporaryMember',
],
];
}