You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Localisation options of the related model are not applied so the model is treated as it always has any option.
Specific example
BusinessListingPage - localised with any option SystemDeal - localised with fallback option BusinessListingPage has many SystemDeal SystemDeal has one BusinessListingPage
TaxonomyTerm - localised with any option TravelAgent - localised with exact option TravelAgent many many TaxonomyTerm TaxonomyTerm belongs many many TravelAgent
Example 1
Inspecting the data list query shows that the system deal relation doesn't include segments representing fallback option.
This solution is applied as extension but ideally this is solved within the Fluent module. We may need some extension points in some of the core modules first though.
<?phpnamespaceApp\ORM\Filters;
useApp\Extensions\Locale\LocalisationOrFallbackRequiredFluentExtension;
useApp\Extensions\Locale\LocalisationRequiredFluentExtension;
useException;
useInvalidArgumentException;
useSilverStripe\Core\Extension;
useSilverStripe\ORM\DataList;
useSilverStripe\ORM\DataObject;
useSilverStripe\ORM\DataObjectSchema;
useSilverStripe\ORM\DataQuery;
useTractorCow\Fluent\Extension\FluentExtension;
useTractorCow\Fluent\Model\Locale;
/** * This extension provides temporary fix for localisation of ORM filters * This fix has limited scope as proper fix needs to be done on the Fluent module and also some other related modules * need to be updated (at least extension points need to be added) * * Limitation of scope: * - available only for filter() method * - doesn't support chained relation lookups * (RelationName.Field is supported, RelationName1.RelationName2.Field is not) * * @property $this|DataList $owner */classFiltersLocalisationExtensionextendsExtension
{
/** * Apply localisation and use filter() method * Same input params as @see DataList::filter() * * @param mixed ...$arguments * @return DataList * @throws Exception */publicfunctionlocalisedFilter(...$arguments): DataList
{
// Validate and process argumentsswitch (sizeof($arguments)) {
case1:
$filters = $arguments[0];
break;
case2:
$filters = [$arguments[0] => $arguments[1]];
break;
default:
thrownewInvalidArgumentException('Incorrect number of arguments passed to filter()');
}
$list = $this->applyLocalisedFilter($filters);
return$list->filter($filters);
}
/** * @param array $filters * @return DataList * @throws Exception */privatefunctionapplyLocalisedFilter(array$filters): DataList
{
$list = $this->owner;
$locale = Locale::getCurrentLocale();
if ($locale === null) {
return$list;
}
// Keep track of tables we've joined so far, so we avoid redundant query conditions$processedTables = [];
foreach ($filtersas$condition => $value) {
$relatedClass = $this->getRelatedClassFromCondition($list->dataClass(), $condition);
if ($relatedClass === null) {
// This is not a relation filter - bail outcontinue;
}
/** @var DataObject|FluentExtension $singleton */$singleton = DataObject::singleton($relatedClass);
if (!$singleton->hasExtension(FluentExtension::class)) {
// Model not localised - bail out as there is no need to localise filterscontinue;
}
if (!$singleton->hasExtension(LocalisationRequiredFluentExtension::class)
&& !$singleton->hasExtension(LocalisationOrFallbackRequiredFluentExtension::class)) {
// Model not strictly localised - bail out as the fallback to base record is acceptablecontinue;
}
// Remove any filter modifiers$relationField = explode(':', $condition);
$relationField = array_shift($relationField);
// Apply the relation, so we can get a relation table alias// Note that this does not produce redundant query join segments// as relations is internally cached within the core functionality$list = $list->applyRelation($relationField, $columnName);
// Extract the table name which is used to join the relation$relationTable = explode('.', $columnName);
$relationTable = array_shift($relationTable);
$relationTable = trim($relationTable, '"');
if (in_array($relationTable, $processedTables)) {
// We've already handled this relation, so we can bail outcontinue;
}
// Mark table as processed$processedTables[] = $relationTable;
$list = $list->alterDataQuery(
staticfunction (DataQuery$query) use ($singleton, $locale, $relationTable): void {
// Determine localised table that we need to join$baseTable = $singleton->baseTable();
$localisedTable = $singleton->getLocalisedTable($baseTable);
if ($singleton->hasExtension(LocalisationRequiredFluentExtension::class)) {
// Localisation required$joinAlias = $singleton->getLocalisedTable($baseTable, $locale->Locale);
$conditions = sprintf('"%s"."ID" IS NOT NULL', $joinAlias);
$joinCondition = sprintf(
'"%1$s"."ID" = "%2$s"."RecordID" AND "%2$s"."Locale" = ?',
$relationTable,
$joinAlias
);
$query->leftJoin(
$localisedTable,
$joinCondition,
$joinAlias,
20,
[$locale->Locale]
);
$query->where($conditions);
} elseif ($singleton->hasExtension(LocalisationOrFallbackRequiredFluentExtension::class)) {
// Localisation or fallback required$localeChain = $locale->getChain();
$conditions = [];
foreach ($localeChainas$joinLocale) {
$joinAlias = $singleton->getLocalisedTable($baseTable, $joinLocale->Locale);
$conditions[] = sprintf('"%s"."ID" IS NOT NULL', $joinAlias);
$joinCondition = sprintf(
'"%1$s"."ID" = "%2$s"."RecordID" AND "%2$s"."Locale" = ?',
$relationTable,
$joinAlias
);
$query->leftJoin(
$localisedTable,
$joinCondition,
$joinAlias,
20,
[$joinLocale->Locale]
);
}
$query->whereAny($conditions);
}
}
);
}
return$list;
}
/** * Determine class of the related model from relation condition * * @param string $class * @param string $condition * @return string|null */privatefunctiongetRelatedClassFromCondition(string$class, string$condition): ?string
{
if (mb_strpos($condition, '.') === false) {
returnnull;
}
$relation = explode('.', $condition);
$relation = array_shift($relation);
$schema = DataObjectSchema::singleton();
// Check has_one$component = $schema->hasOneComponent($class, $relation);
if ($component) {
return$component;
}
// Check has_many$component = $schema->hasManyComponent($class, $relation);
if ($component) {
return$component;
}
// check belongs_to$component = $schema->belongsToComponent($class, $relation);
if ($component) {
return$component;
}
// Check many_many$component = $schema->manyManyComponent($class, $relation);
if ($component) {
return$component['childClass'];
}
returnnull;
}
}
Unit tests
<?phpnamespaceApp\Tests\ORM\Filters;
useApp\Extensions\Locale\LocaleDefaultRecordsExtension;
useApp\ORM\Filters\FiltersLocalisationExtension;
useApp\Pages\BusinessListingPage;
useException;
useSilverStripe\Dev\SapphireTest;
useSilverStripe\ORM\DataList;
useSilverStripe\ORM\FieldType\DBDatetime;
useSilverStripe\ORM\ValidationException;
useSilverStripe\Taxonomy\TaxonomyTerm;
useTractorCow\Fluent\State\FluentState;
classFiltersLocalisationExtensionTestextendsSapphireTest
{
/** * @var string */protectedstatic$fixture_file = 'FiltersLocalisationExtensionTest.yml';
protectedfunctionsetUp(): void
{
FluentState::singleton()->withState(function (FluentState$state): void {
$state->setLocale(LocaleDefaultRecordsExtension::LOCALE_INTERNATIONAL_ENGLISH);
parent::setUp();
});
}
/** * @param string $writeLocale * @param string $targetLocale * @param string $now * @param int $expected * @throws ValidationException * @throws Exception * @dataProvider localisationOrFallbackRequiredDataProvider */publicfunctiontestLocalisationOrFallbackRequired(
string$writeLocale,
string$targetLocale,
string$now,
int$expected
): void {
DBDatetime::set_mock_now($now);
// Fetch the model from source locale/** @var BusinessListingPage $page */$page = FluentState::singleton()->withState(function (FluentState$state): BusinessListingPage {
$state->setLocale(LocaleDefaultRecordsExtension::LOCALE_INTERNATIONAL_ENGLISH);
/** @var BusinessListingPage $page */$page = $this->objFromFixture(BusinessListingPage::class, 'page1');
return$page;
});
// Localise the model to specified localeFluentState::singleton()->withState(staticfunction (FluentState$state) use ($page, $writeLocale): void {
$state->setLocale($writeLocale);
$page->write();
});
// Assert localised filtersFluentState::singleton()->withState(function (FluentState$state) use ($page, $targetLocale, $expected): void {
$state->setLocale($targetLocale);
/** @var BusinessListingPage $page */$page = BusinessListingPage::get()->byID($page->ID);
$this->assertNotNull($page);
/** @var DataList|FiltersLocalisationExtension $pages */$pages = BusinessListingPage::get();
$pages = $pages->localisedFilter([
'SystemDeals.DisplayDateStart:LessThan' => DBDatetime::now()->Rfc2822(),
'SystemDeals.DisplayDateEnd:GreaterThan' => DBDatetime::now()->Rfc2822(),
]);
$this->assertCount($expected, $pages);
});
}
publicfunctionlocalisationOrFallbackRequiredDataProvider(): array
{
return [
'Related model localised / matches filter criteria' => [
LocaleDefaultRecordsExtension::LOCALE_INTERNATIONAL_ENGLISH,
LocaleDefaultRecordsExtension::LOCALE_INTERNATIONAL_ENGLISH,
'2020-01-01 01:00:00',
1,
],
'Related model localised / no matches for filter criteria' => [
LocaleDefaultRecordsExtension::LOCALE_INTERNATIONAL_ENGLISH,
LocaleDefaultRecordsExtension::LOCALE_INTERNATIONAL_ENGLISH,
'2020-01-03 01:00:00',
0,
],
'Related model not localised but with fallback / matches filter criteria' => [
LocaleDefaultRecordsExtension::LOCALE_INTERNATIONAL_ENGLISH,
LocaleDefaultRecordsExtension::LOCALE_UNITED_STATES,
'2020-01-01 01:00:00',
1,
],
'Related model not localised but with fallback / no matches for filter criteria' => [
LocaleDefaultRecordsExtension::LOCALE_INTERNATIONAL_ENGLISH,
LocaleDefaultRecordsExtension::LOCALE_UNITED_STATES,
'2020-01-03 01:00:00',
0,
],
'Related model not localised / matches filter criteria' => [
LocaleDefaultRecordsExtension::LOCALE_JAPAN,
LocaleDefaultRecordsExtension::LOCALE_JAPAN,
'2020-01-01 01:00:00',
0,
],
];
}
/** * @param string $writeLocale * @param string $targetLocale * @param int $oid * @param int $expected * @throws ValidationException * @throws Exception * @dataProvider localisationRequiredDataProvider */publicfunctiontestLocalisationRequired(string$writeLocale, string$targetLocale, int$oid, int$expected): void
{
// Fetch the model from source locale/** @var TaxonomyTerm $term */$term = FluentState::singleton()->withState(function (FluentState$state): TaxonomyTerm {
$state->setLocale(LocaleDefaultRecordsExtension::LOCALE_INTERNATIONAL_ENGLISH);
/** @var TaxonomyTerm $term */$term = $this->objFromFixture(TaxonomyTerm::class, 'term1');
return$term;
});
// Localise the model to specified localeFluentState::singleton()->withState(staticfunction (FluentState$state) use ($term, $writeLocale): void {
$state->setLocale($writeLocale);
$term->write();
});
// Assert localised filtersFluentState::singleton()->withState(
function (FluentState$state) use ($term, $targetLocale, $oid, $expected): void {
$state->setLocale($targetLocale);
/** @var TaxonomyTerm $term */$term = TaxonomyTerm::get()->byID($term->ID);
$this->assertNotNull($term);
/** @var DataList|FiltersLocalisationExtension $terms */$terms = TaxonomyTerm::get();
$terms = $terms->localisedFilter([
'TravelAgents.OID' => $oid,
]);
$this->assertCount($expected, $terms);
}
);
}
publicfunctionlocalisationRequiredDataProvider(): array
{
return [
'Related model localised / matches filter criteria' => [
LocaleDefaultRecordsExtension::LOCALE_INTERNATIONAL_ENGLISH,
LocaleDefaultRecordsExtension::LOCALE_INTERNATIONAL_ENGLISH,
123,
1,
],
'Related model localised / no matches for filter criteria' => [
LocaleDefaultRecordsExtension::LOCALE_INTERNATIONAL_ENGLISH,
LocaleDefaultRecordsExtension::LOCALE_INTERNATIONAL_ENGLISH,
124,
0,
],
'Related model localised but with fallback / matches filter criteria' => [
LocaleDefaultRecordsExtension::LOCALE_INTERNATIONAL_ENGLISH,
LocaleDefaultRecordsExtension::LOCALE_UNITED_STATES,
123,
0,
],
'Related model not localised / matches filter criteria' => [
LocaleDefaultRecordsExtension::LOCALE_JAPAN,
LocaleDefaultRecordsExtension::LOCALE_JAPAN,
123,
0,
],
'Related model not localised / no matches for filter criteria' => [
LocaleDefaultRecordsExtension::LOCALE_JAPAN,
LocaleDefaultRecordsExtension::LOCALE_JAPAN,
124,
0,
],
];
}
}
Another way of doing it without an explicit method is to localise all joins. We'd have to detect all joins in a query, then reverse lookup the table to the class, and checking if those classes are localised. :)
It's very hard to do it in such a way that you don't break complex and custom filters / joins though.
To be honest, I don't really have much of the motivation to do the kind of huge refactor that would provide this kind of functionality.
We could just look at dropping in your solution, but instead of the ->hasExtension checks, add more extension points. I think that's what you want right?
You'd probably want to do a bit of work to make sure fallback is respected too.
BUG: Localisation option is not applied to relation data
frontend_publish_required
/cms_localisation_required
can have the following values:any
- base record data is acceptablefallback
- localisation or fallback is requiredexact
- localisation is requiredThis configuration is applied to regular data lookups but not data lookups which contain relations.
Generic example
Localisation options of the related model are not applied so the model is treated as it always has
any
option.Specific example
BusinessListingPage
- localised withany
optionSystemDeal
- localised withfallback
optionBusinessListingPage
has manySystemDeal
SystemDeal
has oneBusinessListingPage
TaxonomyTerm
- localised withany
optionTravelAgent
- localised withexact
optionTravelAgent
many manyTaxonomyTerm
TaxonomyTerm
belongs many manyTravelAgent
Example 1
Inspecting the data list query shows that the system deal relation doesn't include segments representing
fallback
option.Example 2
Inspecting the data list query shows that the system deal relation doesn't include segments representing
exact
option.Bespoke solution
This solution is applied as extension but ideally this is solved within the Fluent module. We may need some extension points in some of the core modules first though.
Unit tests
The text was updated successfully, but these errors were encountered: