mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
Merge branch '4.11' into 4
This commit is contained in:
commit
cfb347dc9b
@ -61,11 +61,20 @@ public function getCMSFields()
|
||||
You can also alter the fields of built-in and module `DataObject` classes through your own
|
||||
[DataExtension](/developer_guides/extending/extensions), and a call to `DataExtension->updateCMSFields`.
|
||||
|
||||
[info]
|
||||
`FormField` scaffolding takes [`$field_labels` config](#field-labels) into account as well.
|
||||
[/info]
|
||||
|
||||
## Searchable Fields
|
||||
|
||||
The `$searchable_fields` property uses a mixed array format that can be used to further customise your generated admin
|
||||
system. The default is a set of array values listing the fields.
|
||||
|
||||
[info]
|
||||
`$searchable_fields` will default to use the [`$summary_fields` config](#summary-fields) if not defined. This works fine unless
|
||||
your `$summary_fields` config specifies fields that are not stored in the database.
|
||||
[/info]
|
||||
|
||||
```php
|
||||
use SilverStripe\ORM\DataObject;
|
||||
|
||||
@ -79,6 +88,8 @@ class MyDataObject extends DataObject
|
||||
}
|
||||
```
|
||||
|
||||
### Specify a form field or search filter
|
||||
|
||||
Searchable fields will appear in the search interface with a default form field (usually a [TextField](api:SilverStripe\Forms\TextField)) and a
|
||||
default search filter assigned (usually an [ExactMatchFilter](api:SilverStripe\ORM\Filters\ExactMatchFilter)). To override these defaults, you can specify
|
||||
additional information on `$searchable_fields`:
|
||||
@ -119,6 +130,8 @@ class MyDataObject extends DataObject
|
||||
}
|
||||
```
|
||||
|
||||
### Searching on relations
|
||||
|
||||
To include relations (`$has_one`, `$has_many` and `$many_many`) in your search, you can use a dot-notation.
|
||||
|
||||
```php
|
||||
@ -154,23 +167,29 @@ class Player extends DataObject
|
||||
|
||||
```
|
||||
|
||||
Use a single search field that matches on multiple database fields with `'match_any'`
|
||||
### Searching many db fields on a single search field
|
||||
|
||||
Use a single search field that matches on multiple database fields with `'match_any'`. This also supports specifying a field and a filter, though it is not necessary to do so.
|
||||
|
||||
```php
|
||||
class Order extends DataObject
|
||||
{
|
||||
private static $db = [
|
||||
'Name' => 'Varchar',
|
||||
];
|
||||
|
||||
private static $has_one = [
|
||||
'Customer' => Customer::class,
|
||||
'ShippingAddress' => Address::class,
|
||||
];
|
||||
|
||||
private static $searchable_fields = [
|
||||
'CustomFirstName' => [
|
||||
'CustomName' => [
|
||||
'title' => 'First Name',
|
||||
'field' => TextField::class,
|
||||
'filter' => 'PartialMatchFilter',
|
||||
'match_any' => [
|
||||
// Searching with the "First Name" field will show Orders matching either Customer.FirstName or ShippingAddress.FirstName
|
||||
// Searching with the "First Name" field will show Orders matching either Name, Customer.FirstName, or ShippingAddress.FirstName
|
||||
'Name',
|
||||
'Customer.FirstName',
|
||||
'ShippingAddress.FirstName',
|
||||
]
|
||||
@ -179,7 +198,11 @@ class Order extends DataObject
|
||||
}
|
||||
```
|
||||
|
||||
### Summary Fields
|
||||
[alert]
|
||||
If you don't specify a field, you must use the name of a real database field instead of a custom name so that a default field can be determined.
|
||||
[/alert]
|
||||
|
||||
## Summary Fields
|
||||
|
||||
Summary fields can be used to show a quick overview of the data for a specific [DataObject](api:SilverStripe\ORM\DataObject) record. The most common use
|
||||
is their display as table columns, e.g. in the search results of a [ModelAdmin](api:SilverStripe\Admin\ModelAdmin) CMS interface.
|
||||
@ -202,6 +225,8 @@ class MyDataObject extends DataObject
|
||||
}
|
||||
```
|
||||
|
||||
### Relations in summary fields
|
||||
|
||||
To include relations or field manipulations in your summaries, you can use a dot-notation.
|
||||
|
||||
```php
|
||||
@ -234,6 +259,8 @@ class MyDataObject extends DataObject
|
||||
|
||||
```
|
||||
|
||||
### Images in summary fields
|
||||
|
||||
Non-textual elements (such as images and their manipulations) can also be used in summaries.
|
||||
|
||||
```php
|
||||
@ -257,7 +284,9 @@ class MyDataObject extends DataObject
|
||||
|
||||
```
|
||||
|
||||
In order to re-label any summary fields, you can use the `$field_labels` static.
|
||||
## Field labels
|
||||
|
||||
In order to re-label any summary fields, you can use the `$field_labels` static. This will also affect the output of `$object->fieldLabels()` and `$object->fieldLabel()`.
|
||||
|
||||
```php
|
||||
use SilverStripe\ORM\DataObject;
|
||||
@ -283,6 +312,7 @@ class MyDataObject extends DataObject
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
## Related Documentation
|
||||
|
||||
* [SearchFilters](searchfilters)
|
||||
|
@ -151,7 +151,7 @@ class MySQLiConnector extends DBConnector
|
||||
|
||||
public function escapeString($value)
|
||||
{
|
||||
return $this->dbConn->real_escape_string($value);
|
||||
return $this->dbConn->real_escape_string($value ?? '');
|
||||
}
|
||||
|
||||
public function quoteString($value)
|
||||
@ -181,7 +181,7 @@ class MySQLiConnector extends DBConnector
|
||||
$this->beforeQuery($sql);
|
||||
|
||||
// Benchmark query
|
||||
$handle = $this->dbConn->query($sql, MYSQLI_STORE_RESULT);
|
||||
$handle = $this->dbConn->query($sql ?? '', MYSQLI_STORE_RESULT);
|
||||
|
||||
if (!$handle || $this->dbConn->error) {
|
||||
$this->databaseError($this->getLastError(), $errorLevel, $sql);
|
||||
@ -319,7 +319,7 @@ class MySQLiConnector extends DBConnector
|
||||
|
||||
public function selectDatabase($name)
|
||||
{
|
||||
if ($this->dbConn->select_db($name)) {
|
||||
if ($this->dbConn->select_db($name ?? '')) {
|
||||
$this->databaseName = $name;
|
||||
return true;
|
||||
}
|
||||
|
@ -110,10 +110,15 @@ class PDOConnector extends DBConnector implements TransactionManager
|
||||
}
|
||||
|
||||
// Generate new statement
|
||||
$statement = $this->pdoConnection->prepare(
|
||||
$sql,
|
||||
[PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY]
|
||||
);
|
||||
try {
|
||||
$statement = $this->pdoConnection->prepare(
|
||||
$sql,
|
||||
[PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY]
|
||||
);
|
||||
} catch (PDOException $e) {
|
||||
$statement = false;
|
||||
$this->databaseError($e->getMessage(), E_USER_ERROR, $sql);
|
||||
}
|
||||
|
||||
// Wrap in a PDOStatementHandle, to cache column metadata
|
||||
$statementHandle = ($statement === false) ? false : new PDOStatementHandle($statement);
|
||||
@ -558,16 +563,13 @@ class PDOConnector extends DBConnector implements TransactionManager
|
||||
}
|
||||
}
|
||||
|
||||
// Note: $this->inTransaction may not match the 'in-transaction' state in PDO
|
||||
$this->inTransaction = false;
|
||||
try {
|
||||
if ($this->pdoConnection->inTransaction()) {
|
||||
return $this->pdoConnection->rollBack();
|
||||
} catch (PDOException $e) {
|
||||
// A PDOException will be thrown if there is no active transaction in PHP 8+
|
||||
// Prior to PHP 8, this failed silently, so returning false here is backwards compatible
|
||||
// Note: $this->inTransaction may not match the 'in-transaction' state in PDO
|
||||
// https://www.php.net/manual/en/pdo.rollback.php
|
||||
return false;
|
||||
}
|
||||
// return false because it did not rollback.
|
||||
return false;
|
||||
}
|
||||
|
||||
public function transactionDepth()
|
||||
|
@ -17,6 +17,7 @@ use SilverStripe\Forms\SelectField;
|
||||
use SilverStripe\Forms\CheckboxField;
|
||||
use InvalidArgumentException;
|
||||
use Exception;
|
||||
use SilverStripe\ORM\DataQuery;
|
||||
|
||||
/**
|
||||
* Manages searching of properties on one or more {@link DataObject}
|
||||
@ -74,7 +75,8 @@ class SearchContext
|
||||
protected $searchParams = [];
|
||||
|
||||
/**
|
||||
* The logical connective used to join WHERE clauses. Defaults to AND.
|
||||
* The logical connective used to join WHERE clauses. Must be "AND".
|
||||
* @deprecated 5.0
|
||||
* @var string
|
||||
*/
|
||||
public $connective = 'AND';
|
||||
@ -146,6 +148,10 @@ class SearchContext
|
||||
*/
|
||||
public function getQuery($searchParams, $sort = false, $limit = false, $existingQuery = null)
|
||||
{
|
||||
if ($this->connective != "AND") {
|
||||
throw new Exception("SearchContext connective '$this->connective' not supported after ORM-rewrite.");
|
||||
}
|
||||
|
||||
/** DataList $query */
|
||||
$query = null;
|
||||
if ($existingQuery) {
|
||||
@ -174,28 +180,25 @@ class SearchContext
|
||||
$query = $query->sort($sort);
|
||||
$this->setSearchParams($searchParams);
|
||||
|
||||
$modelObj = Injector::inst()->create($this->modelClass);
|
||||
$searchableFields = $modelObj->searchableFields();
|
||||
foreach ($this->searchParams as $key => $value) {
|
||||
$key = str_replace('__', '.', $key ?? '');
|
||||
if ($filter = $this->getFilter($key)) {
|
||||
$filter->setModel($this->modelClass);
|
||||
$filter->setValue($value);
|
||||
if (!$filter->isEmpty()) {
|
||||
$modelObj = Injector::inst()->create($this->modelClass);
|
||||
if (isset($modelObj->searchableFields()[$key]['match_any'])) {
|
||||
$query = $query->alterDataQuery(function ($dataQuery) use ($modelObj, $key, $value) {
|
||||
$searchFields = $modelObj->searchableFields()[$key]['match_any'];
|
||||
$sqlSearchFields = [];
|
||||
foreach ($searchFields as $dottedRelation) {
|
||||
$relation = substr($dottedRelation ?? '', 0, strpos($dottedRelation ?? '', '.'));
|
||||
$relations = explode('.', $dottedRelation ?? '');
|
||||
$fieldName = array_pop($relations);
|
||||
$relationModelName = $dataQuery->applyRelation($relation);
|
||||
$relationPrefix = $dataQuery->applyRelationPrefix($relation);
|
||||
$columnName = $modelObj->getSchema()
|
||||
->sqlColumnForField($relationModelName, $fieldName, $relationPrefix);
|
||||
$sqlSearchFields[$columnName] = $value;
|
||||
if (isset($searchableFields[$key]['match_any'])) {
|
||||
$searchFields = $searchableFields[$key]['match_any'];
|
||||
$filterClass = get_class($filter);
|
||||
$modifiers = $filter->getModifiers();
|
||||
$query = $query->alterDataQuery(function (DataQuery $dataQuery) use ($searchFields, $filterClass, $modifiers, $value) {
|
||||
$subGroup = $dataQuery->disjunctiveGroup();
|
||||
foreach ($searchFields as $matchField) {
|
||||
/** @var SearchFilter $filterClass */
|
||||
$filter = new $filterClass($matchField, $value, $modifiers);
|
||||
$filter->apply($subGroup);
|
||||
}
|
||||
$dataQuery = $dataQuery->whereAny($sqlSearchFields);
|
||||
});
|
||||
} else {
|
||||
$query = $query->alterDataQuery([$filter, 'apply']);
|
||||
@ -204,10 +207,6 @@ class SearchContext
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->connective != "AND") {
|
||||
throw new Exception("SearchContext connective '$this->connective' not supported after ORM-rewrite.");
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
|
@ -262,10 +262,48 @@ class SearchContextTest extends SapphireTest
|
||||
|
||||
// Search should match Order's customer FirstName
|
||||
$results = $context->getResults(['CustomFirstName' => 'Bill']);
|
||||
$this->assertEquals(1, $results->Count());
|
||||
$this->assertCount(2, $results);
|
||||
$this->assertListContains([
|
||||
['Name' => 'Jane'],
|
||||
['Name' => 'Jack'],
|
||||
], $results);
|
||||
|
||||
// Search should match Order's shipping address FirstName
|
||||
$results = $context->getResults(['CustomFirstName' => 'Bob']);
|
||||
$this->assertEquals(1, $results->Count());
|
||||
$this->assertCount(2, $results);
|
||||
$this->assertListContains([
|
||||
['Name' => 'Jane'],
|
||||
['Name' => 'Jill'],
|
||||
], $results);
|
||||
|
||||
// Search should match Order's Name db field
|
||||
$results = $context->getResults(['CustomFirstName' => 'Jane']);
|
||||
$this->assertCount(1, $results);
|
||||
$this->assertSame('Jane', $results->first()->Name);
|
||||
|
||||
// Search should not match any Order
|
||||
$results = $context->getResults(['CustomFirstName' => 'NoMatches']);
|
||||
$this->assertCount(0, $results);
|
||||
}
|
||||
|
||||
public function testMatchAnySearchWithFilters()
|
||||
{
|
||||
$order1 = $this->objFromFixture(SearchContextTest\Order::class, 'order1');
|
||||
$context = $order1->getDefaultSearchContext();
|
||||
|
||||
$results = $context->getResults(['ExactMatchField' => 'Bil']);
|
||||
$this->assertCount(0, $results);
|
||||
$results = $context->getResults(['PartialMatchField' => 'Bil']);
|
||||
$this->assertCount(2, $results);
|
||||
|
||||
$results = $context->getResults(['ExactMatchField' => 'ob']);
|
||||
$this->assertCount(0, $results);
|
||||
$results = $context->getResults(['PartialMatchField' => 'ob']);
|
||||
$this->assertCount(2, $results);
|
||||
|
||||
$results = $context->getResults(['ExactMatchField' => 'an']);
|
||||
$this->assertCount(0, $results);
|
||||
$results = $context->getResults(['PartialMatchField' => 'an']);
|
||||
$this->assertCount(1, $results);
|
||||
}
|
||||
}
|
||||
|
@ -74,12 +74,29 @@ SilverStripe\ORM\Tests\Search\SearchContextTest\AllFilterTypes:
|
||||
SilverStripe\ORM\Tests\Search\SearchContextTest\Customer:
|
||||
customer1:
|
||||
FirstName: Bill
|
||||
customer2:
|
||||
FirstName: Bailey
|
||||
customer3:
|
||||
FirstName: Billy
|
||||
|
||||
SilverStripe\ORM\Tests\Search\SearchContextTest\Address:
|
||||
address1:
|
||||
FirstName: Bob
|
||||
address2:
|
||||
FirstName: Barley
|
||||
address3:
|
||||
FirstName: Billy
|
||||
|
||||
SilverStripe\ORM\Tests\Search\SearchContextTest\Order:
|
||||
order1:
|
||||
Name: 'Jane'
|
||||
Customer: =>SilverStripe\ORM\Tests\Search\SearchContextTest\Customer.customer1
|
||||
ShippingAddress: =>SilverStripe\ORM\Tests\Search\SearchContextTest\Address.address1
|
||||
order2:
|
||||
Name: 'Jill'
|
||||
Customer: =>SilverStripe\ORM\Tests\Search\SearchContextTest\Customer.customer2
|
||||
ShippingAddress: =>SilverStripe\ORM\Tests\Search\SearchContextTest\Address.address1
|
||||
order3:
|
||||
Name: 'Jack'
|
||||
Customer: =>SilverStripe\ORM\Tests\Search\SearchContextTest\Customer.customer3
|
||||
ShippingAddress: =>SilverStripe\ORM\Tests\Search\SearchContextTest\Address.address3
|
||||
|
@ -10,6 +10,10 @@ class Order extends DataObject implements TestOnly
|
||||
{
|
||||
private static $table_name = 'SearchContextTest_Order';
|
||||
|
||||
private static $db = [
|
||||
'Name' => 'Varchar',
|
||||
];
|
||||
|
||||
private static $has_one = [
|
||||
'Customer' => Customer::class,
|
||||
'ShippingAddress' => Address::class,
|
||||
@ -19,12 +23,30 @@ class Order extends DataObject implements TestOnly
|
||||
'CustomFirstName' => [
|
||||
'title' => 'First Name',
|
||||
'field' => TextField::class,
|
||||
'filter' => 'PartialMatchFilter',
|
||||
'match_any' => [
|
||||
// Searching with "First Name" will show Orders with matching Customer or Address names
|
||||
// Searching with the "First Name" field will show Orders matching either Name, Customer.FirstName, or ShippingAddress.FirstName
|
||||
'Name',
|
||||
'Customer.FirstName',
|
||||
'ShippingAddress.FirstName',
|
||||
]
|
||||
]
|
||||
],
|
||||
],
|
||||
'PartialMatchField' => [
|
||||
'field' => TextField::class,
|
||||
'filter' => 'PartialMatchFilter',
|
||||
'match_any' => [
|
||||
'Name',
|
||||
'Customer.FirstName',
|
||||
'ShippingAddress.FirstName',
|
||||
],
|
||||
],
|
||||
'ExactMatchField' => [
|
||||
'field' => TextField::class,
|
||||
'filter' => 'ExactMatchFilter',
|
||||
'match_any' => [
|
||||
'Name',
|
||||
'Customer.FirstName',
|
||||
'ShippingAddress.FirstName',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user