Question

EDIT:

Because I was to late with awarding the initial bounty of 300 to @arcain I'm reopening. And awarding the additional 150 to @arcain. Unless of course somebody provides even a better answer. :)

/ EDIT

Consider the following form:

language | region | active | default |
-----------------------------------------------
en       | GB     | [x]    | (*)     | [X delete]
nl       | NL     | [x]    | ( )     | [X delete]
nl       | BE     | [x]    | ( )     | [X delete]

[x] let visitors browser-settings determine the default language

[save]

The settings of the above table will be saved in a DB table which columns map to the above columns (excluding the last column obviously).

All (save & delete) actions direct to a Localization controller. The Localization controller basically calls methods on a LocalizationService, like so:

$localizationService->updateCollection( $_POST ) // update collection settings
// or
$localizationService->delete( $_POST ) // delete a single locale

The LocalizationService in it's turn calls a LocaleMapperDb, something like this:

foreach( $localeCollection as $locale )
{
    $localeMapperDb->update( LocaleModel $locale );
}
// or
$localeMapperDb->delete( LocaleModel $locale );

Where though, is the responsibility for saving this setting:

[x] let visitors browser-settings determine default language

It will be saved in a DB table called site_settings. I have thought of a few options:

  • Use a SiteService / SiteSettingsService in the LocalizationController. But then, the complete form is generated and processed in the LocalizationService already.
  • Use a SiteMapperDb / SiteSettingsMapperDb in the LocalizationService and use it in updateCollection( $_POST )
  • Use a SiteMapperDb / SiteSettingsMapperDb in the LocaleMapperDb

The first and last options look like the worst options, but I'm unsure. What do you feel is the best option? Or maybe you have an alternative option, I haven't thought of?

Was it helpful?

Solution

I think projecting domain model objects onto view model objects works well in this situation.

In the case of the attached code (please pardon me for writing it in C#; it should be fairly portable) the domain model objects are never exposed (they only are accessed directly within the service objects.) The services only expose view model objects, like LocalViewModel, and those view model objects are manipulated by the controllers.

The LocaleConfigController also maps the data returned by the services into a LocaleConfigViewModel object, and this object is the only object that is exchanged directly with the view.

So, in a nutshell, the view has a dedicated controller, and the view communicates with the controller via the LocaleConfigViewModel object. The controller manipulates the LocaleConfigViewModel object and calls into implementations of the ILocaleConfigService and the ISystemConfigService. The service objects never expose the domain model to the controller, and they are responsible for mapping view model objects to domain model objects (via whatever persistence mechanism is desirable.)

Note that the locale service is a configuration service, it would not have any implementation to look up what the correct localized strings would be. I would put that into another service, because it could be used in places where you would not want to expose any methods that would allow the alteration of the localization config.

For example, in the management side of the application, you would want both the localization config service and localization string rendering service (since the management site could be localized as well.) For a customer facing front end, you would instead probably want only the localization string rendering service, because system configuration modifications should be unwanted and out of scope for that site.

So, to finally answer your question: the controller contains references to both the locale and system configuration services, and the controller is dedicated to the view -- it has a well-defined contract where only LocaleConfigViewModels are exchanged.

As for where the responsibility lies for saving the system-wide settings, the controller is responsible for unpacking the system settings from the LocaleConfigViewModel and pushing them into the appropriate services (in this case the ISystemConfigService instance) where they will be persisted.

class LocaleViewModel
{
  public int Id;
  public string Language;
  public string Region;
  public bool Enabled;
  public bool Deleted;
}

class LocaleConfigViewModel
{
  public bool UseVisitorBrowserLocale;
  public LocaleViewModel DefaultLocale;
  public List<LocaleViewModel> Locales; 
}

class LocaleConfigController : ILocaleConfigController
{
  ILocaleConfigService localeConfig;
  ISystemConfigService systemConfig;

  public void Save(LocaleConfigViewModel model)
  {
    foreach (var locale in model.Locales)
    {
      if (locale.Deleted)
      {
        localeConfig.DeleteLocale(locale);
        continue;
      }
      localeConfig.UpdateLocale(locale);
    }
    systemConfig.DefaultLocaleId = model.DefaultLocale.Id;
    systemConfig.UseVisitorBrowserLocale = model.UseVisitorBrowserLocale;
  }

  public LocaleConfigViewModel GetCurrentView()
  {
    var model = new LocaleConfigViewModel();
    model.Locales = localeConfig.Locales;
    model.DefaultLocale = model.Locales.FirstOrDefault(l => l.Id == systemConfig.DefaultLocaleId);
    model.UseVisitorBrowserLocale = systemConfig.UseVisitorBrowserLocale;
    return model;
  }

  // ...
}

interface ILocaleConfigController
{
  void Save(LocaleConfigViewModel model);
  LocaleConfigViewModel GetCurrentView();
  // ... 
}

interface ILocaleConfigService // services will be stateless and threadsafe
{
  void DeleteLocale(LocaleViewModel locale);
  void UpdateLocale(LocaleViewModel locale);
  List<LocaleViewModel> Locales { get; }
  // ...
}

interface ISystemConfigService // services will be stateless and threadsafe
{
  int DefaultLocaleId { get; set; }
  bool UseVisitorBrowserLocale { get; set; }
  // ...
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top