Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FIX Image in summaryfields breaks search #10885

Conversation

sabina-talipova
Copy link
Contributor

@sabina-talipova sabina-talipova commented Jul 24, 2023

Description

Fields that pass validation on hasMethod() check, but do not have a corresponding field or relations in the DB are excluded from searchable_fields.

Parent issue

@sabina-talipova sabina-talipova force-pushed the pulls/4.13/search-in-non-existing-fields branch 5 times, most recently from e466b85 to c65b5d5 Compare July 31, 2023 03:47
@sabina-talipova sabina-talipova marked this pull request as ready for review July 31, 2023 04:00
Copy link
Member

@GuySartorelli GuySartorelli left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haven't tested locally yet but there are some things in the implementation and tests that don't quite make sense to me

src/ORM/DataObject.php Outdated Show resolved Hide resolved
src/ORM/DataObject.php Outdated Show resolved Hide resolved
src/ORM/DataObject.php Outdated Show resolved Hide resolved
tests/php/ORM/Search/SearchContextTest.php Outdated Show resolved Hide resolved
@sabina-talipova sabina-talipova force-pushed the pulls/4.13/search-in-non-existing-fields branch from c65b5d5 to df0981e Compare July 31, 2023 23:36
@sabina-talipova sabina-talipova force-pushed the pulls/4.13/search-in-non-existing-fields branch from df0981e to 0c5290a Compare August 1, 2023 00:08
@GuySartorelli
Copy link
Member

GuySartorelli commented Aug 1, 2023

I think this approach isn't complete.

I have used the following class when testing locally. Note that the Company class mentioned is just some random DataObject with a Name field.

use SilverStripe\Assets\Image;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\FieldType\DBField;

class SearchableFieldsTest extends DataObject
{
    private static $db = [
        'Name' => 'Varchar',
        'Biography' => 'HTMLText',
    ];

    private static $has_one = [
        'Company' => Company::class,
        'ProfileImage' => Image::class
    ];

    private static $many_many  = [
        'PastCompanies' => Company::class
    ];

    private static $summary_fields = [
        'ID',
        'Name',
        'Biography.Plain',
        'CompanyID',
        'Company.Category',
        'Company.Name.Plain',
        'PastCompanies.Name',
        'PastCompanies.Count', // shouldn't be searchable
        'ProfileImage.CMSThumbnail', //  shouldn't be searchable
        'MethodOne', //  shouldn't be searchable
        'MethodTwo', //  shouldn't be searchable
        'Company', //  shouldn't be searchable
    ];

    public function getMethodOne()
    {
        return implode(' ', $this->Company()->Employees()->column('Name'));
    }

    public function MethodTwo()
    {
        return DBField::create_field('Varchar', 'this is the value');
    }
}

Problems with the current implementation

Correctly included:

  • Direct fields on the class (e.g. Name)
  • Direct fields on the class, even if we're calling Plain or Nice etc on that field (e.g. Biography.Plain)
  • Direct fields on a has_one relation (e.g. Company.Category)
  • Direct fields on a many_many or has_many relation (e.g. PastCompanies.Name)

Still included, but shouldn't be:

  • Methods on the class (e.g. MethodTwo)

Not included, but should be:

  • Direct fields on a relation if we're calling Plain or Nice etc on that field (e.g. Company.Name.Plain)

Not tested:

  • Direct fields on chained relations (e.g. Company.Customers.Location)
  • Direct fields on chained relations if we're calling Plain or Nice etc on that field (e.g. Company.Customers.Location.Plain)

Recommendation

Instead of hasRelations(), which is still allowing methods and isn't correctly allowing relation fields in some cases, have a method, similar to the below, which takes a dot-syntax field name (e.g. Name or Company.Customers.Location.Plain) and returns only the part that is backed by the database (e.g. Name or Company.Customers.Location).

Note that this implementation is based heavily on relObject(), but has been changed so that any time we come across something that cannot be backed by the database (e.g. a method call that isn't for getting a relation list), we return null - and otherwise we return the dot syntax field name for the database-backed field, which excludes method calls such as Plain.
This works correctly for chained relations.

    private function getDatabaseBackedField(string $fieldPath): ?string
    {
        $component = $this;
        $fieldParts = [];

        foreach (explode('.', $fieldPath ?? '') as $nextPart) {
            if (!$component) {
                return null;
            }
            $fieldParts[] = $nextPart;

            if ($component instanceof Relation || $component instanceof DataList) {
                if ($component->hasMethod($nextPart)) {
                    // If the next part is a method, we don't have a database-backed field.
                    return null;
                }
                // The next part could either be a field, or another relation
                $singleton = DataObject::singleton($component->dataClass());
                if ($singleton->dbObject($nextPart)) {
                    // If the next part is a DBField, we've found the database-backed field.
                    break;
                }
                $component = $component->relation($nextPart);
            } elseif ($component instanceof DataObject && ($component->dbObject($nextPart) instanceof DBField)) {
                // If the next part is a DBField, we've found the database-backed field.
                break;
            } elseif ($component instanceof DataObject && $component->getRelationType($nextPart) !== null) {
                $component = $component->$nextPart();
            } elseif (ClassInfo::hasMethod($component, $nextPart)) {
                // If the next part is a method, we don't have a database-backed field.
                return null;
            } else {
                return null;
            }
        }
        return implode('.', $fieldParts) ?: null;
    }

Then, use this instead of everything in the foreach ($summaryFields as $key => $name) { block:

- $spec = $name;
- // Extract field name in case this is a method called on a field (e.g. "Date.Nice")
- if (($fieldPos = strpos($name ?? '', '.')) !== false) {
-     $name = substr($name ?? '', 0, $fieldPos);
- }
- 
- if ($schema->fieldSpec($this, $name)) {
-     $fields[] = $name;
- } elseif ($this->relObject($spec) && $this->hasRelations($spec)) {
-     $fields[] = $spec;
+ if ($field = $this->getDatabaseBackedField($name)) {
+     $fields[] = $field;
 }

@sabina-talipova sabina-talipova force-pushed the pulls/4.13/search-in-non-existing-fields branch 3 times, most recently from 1188c80 to a6fbcfa Compare August 2, 2023 00:54
tests/php/Forms/GridField/GridFieldFilterHeaderTest.php Outdated Show resolved Hide resolved
tests/php/ORM/DataObjectTest.php Outdated Show resolved Hide resolved
tests/php/ORM/DataObjectTest.php Outdated Show resolved Hide resolved
tests/php/ORM/DataObjectTest.php Outdated Show resolved Hide resolved
tests/php/ORM/DataObjectTest.php Outdated Show resolved Hide resolved
tests/php/ORM/DataObjectTest.php Outdated Show resolved Hide resolved
tests/php/ORM/DataObjectTest.php Outdated Show resolved Hide resolved
tests/php/ORM/Search/SearchContextTest.php Outdated Show resolved Hide resolved
@sabina-talipova sabina-talipova force-pushed the pulls/4.13/search-in-non-existing-fields branch 2 times, most recently from 75f092b to 85421a5 Compare August 3, 2023 00:13
Copy link
Member

@GuySartorelli GuySartorelli left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is shaping up nicely. Just a few small things to clean up now.

tests/php/Forms/GridField/GridFieldFilterHeaderTest.php Outdated Show resolved Hide resolved
src/ORM/DataObject.php Outdated Show resolved Hide resolved
src/ORM/DataObject.php Show resolved Hide resolved
src/ORM/DataObject.php Outdated Show resolved Hide resolved
@sabina-talipova sabina-talipova force-pushed the pulls/4.13/search-in-non-existing-fields branch from 85421a5 to d24095a Compare August 3, 2023 02:49
Copy link
Member

@GuySartorelli GuySartorelli left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, works well locally, and provides a much nicer default behaviour than the previous behaviour.

@GuySartorelli GuySartorelli merged commit 9e5411e into silverstripe:4.13 Aug 3, 2023
12 checks passed
@GuySartorelli GuySartorelli deleted the pulls/4.13/search-in-non-existing-fields branch August 3, 2023 03:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants