2015-04-23 7 views
6

Ich entwickle eine Web-Anwendung, die eine Zeitleiste ähnlich wie eine Facebook-Timeline bietet. Die Zeitleiste selbst ist vollständig generisch und steckbar. Es ist nur eine generische Sammlung von Elementen. Alles mit der richtigen Schnittstelle (Dateable) kann zur Sammlung hinzugefügt und auf der Timeline angezeigt werden.Wie man Filter für eine generische, steckbare Sammlung entwirft?

Andere Komponenten (Symfony-Bundles) definieren Modelle, die die Schnittstelle Dateable implementieren, und richten einen Anbieter ein, der diese Modelle finden und zurückgeben kann. Der Code ist ähnlich wie folgt aus:

class Timeline 
{ 
    private $providers = []; // Filled by DI 

    public function find(/* ... */) 
    { 
     $result = []; 
     foreach ($this->providers as $provider) { 
      $result = array_merge($result, $provider->find(/* ... */)); 
     } 

     return $result; 
    } 
} 

Das Problem ist, dass es eine Reihe von Filtern neben der Timeline werden muss. Einige Filteroptionen (wie date) gelten für alle Anbieter. Aber die meisten Optionen nicht. Zum Beispiel können die meisten Provider die Filteroption author verwenden, aber nicht alle. Einige Benachrichtigungspunkte werden dynamisch generiert und haben keinen Autor.

Einige Filteroptionen gelten nur für einen einzelnen Anbieter. Beispielsweise haben nur Ereigniselemente eine Eigenschaft location.

Ich kann nicht herausfinden, wie man eine Filterform entwirft, die genauso modular wie die Zeitachse selbst ist. Wo sollten die verfügbaren Filteroptionen definiert sein? Bündelspezifische Filteroptionen könnten wahrscheinlich aus dem Bündel selbst stammen, aber wie wäre es mit Filteroptionen (wie user), die von mehreren Bündeln verwendet werden können? Und was ist, wenn einige Filteroptionen später von mehreren Bündeln verwendet werden können? Zum Beispiel haben nur Ereignisse jetzt eine location, aber was ist, wenn ein anderes Modul hinzugefügt wird, das auch Objekte mit einem Standort hat?

Und wie wird jeder Anbieter feststellen, ob das übergebene Filterformular nur Optionen enthält, die es versteht? Wenn ich einen Speicherort im Filter festlege, sollte BlogPostProvider keine Nachrichten zurückgeben, da Blogposts keinen Speicherort haben. Aber ich kann nicht auf location im Filter überprüfen, weil das BlogPostBundle nicht über andere Anbieter und ihre Filteroptionen wissen sollte.

Irgendwelche Ideen, wie ich solch eine Filterform entwerfen kann?

Antwort

0

So habe ich es am Ende gelöst.

Zunächst einmal habe ich eine FilterRegistry. Jedes Bündel kann mithilfe eines Symfony-DI-Tags Filter hinzufügen. Ein Filter ist einfach ein Formulartyp.Beispiel Filter:

class LocationFilterType extends AbstractType 
{ 
    public function buildForm(FormBuilderInterface $builder, array $options) 
    { 
     $builder->add('location', 'choice', [ /* ... */ ]); 
    } 
} 

DI-Konfiguration:

<service id="filter.location" class="My\Bundle\Form\LocationFilterType"> 
    <tag name="form.type" alias="filter_location" /> 
    <tag name="filter.type" alias="location" /> 
</service> 

Die FilterRegistry weiß, wie diese Formulartypen aus dem DI-Container zu bekommen:

class FilterRegistry 
{ 
    public function getFormType($name) 
    { 
     if (!isset($this->types[$name])) { 
      throw new \InvalidArgumentException(sprintf('Unknown filter type "%s"', $name)); 
     } 

     return $this->container->get($this->types[$name]); 
    } 
} 

Die Timeline Klasse und der Dienstanbieter verwenden das FilterBuilder um dem Filterformular neue Filter hinzuzufügen. Der Builder sieht so aus:

class FilterBuilder 
{ 
    public function __construct(FilterRegistry $filterRegistry, FormBuilderInterface $formBuilder) 
    { 
     $this->filterRegistry = $filterRegistry; 
     $this->formBuilder = $formBuilder; 
    } 

    public function add($name) 
    { 
     if ($this->formBuilder->has($name)) { 
      return; 
     } 

     $type = $this->filterRegistry->getFormType($name); 
     $type->buildForm($this->formBuilder, $this->formBuilder->getOptions()); 

     return $this; 
    } 
} 

Um das Formular anzuzeigen, wird ein Filter mit den Optionen aller Anbieter erstellt. Dies geschieht in Timeline->getFilterForm(). Beachten Sie, dass kein Datenobjekt an die Form gebunden:

class Timeline 
{ 
    public function getFilterForm() 
    { 
     $formBuilder = $this->formFactory->createNamedBuilder('', 'base_filter_type'); 

     foreach ($this->providers as $provider) { 
      $provider->configureFilter(new FilterBuilder($this->filterRegistry, $formBuilder)); 
     } 

     return $formBuilder->getForm(); 
    } 
} 

Jeder Provider implementiert die configureFilter Methode:

class EventProvider 
{ 
    public function configureFilter(FilterBuilder $builder) 
    { 
     $builder 
      ->add('location') 
      ->add('author') 
     ; 
    } 
} 

Die find Methode der Timeline-Klasse wird ebenfalls geändert. Anstatt einen Filter mit allen Optionen zu erstellen, erstellt er ein neues Filterformular mit den Optionen für diesen Anbieter. Wenn die Formularvalidierung fehlschlägt, kann der Anbieter die aktuell übergebene Filterkombination nicht verarbeiten. In der Regel liegt dies daran, dass eine Filteroption festgelegt wurde, die der Anbieter nicht versteht. In diesem Fall schlägt die Formularvalidierung fehl, da zusätzliche Daten festgelegt werden.

class Timeline 
{ 
    public function find(Request $request) 
    { 
     $result = []; 

     foreach ($this->providers as $provider) { 
      $filter = $provider->createFilter(); 
      $formBuilder = $this->formFactory->createNamedBuilder('', 'base_filter_type', $filter); 

      $provider->configureFilter(new FilterBuilder($this->filterRegistry, $formBuilder)); 

      $form = $formBuilder->getForm(); 
      $form->handleRequest($request); 

      if (!$form->isSubmitted() || $form->isValid()) { 
       $result = array_merge($result, $provider->find($filter)); 
      } 
     } 

     return $result; 
    } 
} 

In diesem Fall gibt es ist eine Datenklasse an die Form gebunden. $provider->createFilter() gibt einfach ein Objekt mit Eigenschaften zurück, die den Filtern entsprechen. Das gefüllte und validierte Filterobjekt wird dann an die find() Methode des Providers übergeben. ZB:

class EventProvider 
{ 
    public function createFilter() 
    { 
     return new EventFilter(); 
    } 

    public function find(EventFilter $filter) 
    { 
     // Do something with $filter and return events 
    } 
} 

class EventFilter 
{ 
    public $location; 
    public $author; 
} 

Das Ganze zusammen macht es ziemlich einfach, die Filter zu verwalten.

Um eine neue Art von Filter hinzufügen:

  • Implementieren eines Formtype
  • Tag in der DI als form.typeund als filter.type

unter Verwendung eines Filters zu starten:

  • Fügen Sie es dem Filte hinzu rBuilder in configureFilters()
  • eine Eigenschaft zum Filtermodell hinzufügen
  • die Eigenschaft in der find() Methode Griff
0

Fügen Sie eine zentrale FilterHandler hinzu, in der jeder verfügbare Filter registriert werden kann. Generische Filter können im selben Bundle wie der Handler aufbewahrt und von dort registriert werden. Bundles können auch Filter registrieren.

Alle Anbieter sollten wissen, ob und wann welche Filter verwendet werden (nach dem Namen des Filters). Auch DI der Handler in ihnen.

Aus dem Handler können Sie die vollständige Liste der registrierten Filter erhalten und dann Ihre Filterform aufbauen.

Beim Filtern Anruf $provider->filter($requestedFiltersWithValues), die überprüfen wird, ob die Filter, die es verwendet, tatsächlich angefordert und registriert sind (über den injizierten Handler) und Ergebnisse nach Bedarf zurückgeben.

Verwandte Themen