• Willkommen im Zend Framework Forum

    ZF1 Zend Framework 1 + ZF2 Zend Framework 2

    Das Zend Framework Forum ist seit 2006 die erste Anlaufstelle für Zend Framework Entwickler in Deutschland. Mit über 70.000 Beiträgen und einer steigenden Nutzerzahl bietet das Forum hilfreiche Themen und ZF-Tutorials für professionelle Entwickler, fortgeschrittene Programmierer sowie Zend Framework Einsteiger.
    Wenn dies Dein erster Besuch in der Zend Framework Community ist, lies bitte zuerst die Hilfe - FAQ durch. Du musst Dich registrieren, bevor Du Beiträge verfassen kannst. Klicke oben auf 'Registrieren', um die Registrierung zu starten. Du kannst auch jetzt schon Beiträge lesen. Hier im Forum findest Du die Zend Framework Hilfe, die Du suchst!

    Grüße an alle Zend Framework Entwickler. Das Team vom Zend Framework Forum!

    Drupal Agentur

Wie ist eine MultiCheckbox mit aus Objekten kommenden Werten zu handhaben?

automatix

New member
Hallo zusammen!

Die Frage ist etwas verwirrend formuliert, ich versuche zu erklären, was ich meine:

Ich habe ein verschachteltes Formular mit mehreren Sub-Fieldsets. Dieses Formular ist parallel zu einem verschachtelten Objekt aufgebaut, also in etwa wie:

Code:
[TypeFoo: 'use_as_base_fieldset' => true]
property $a int
property $b TypeB
    property $c string
    property $d TypeD
        property $e string
        property $f string
    property $g TypeG
        property $h string
        property $i array (mit Elementen TypeI)
...

[MyForm] > [FieldsetFoo]
element 'a' Text
fieldset FieldsetB
    element 'c' string
    fieldset FieldsetD
        element 'e' string
        element 'f' string
    fieldset FieldsetG
        element 'h' string
        collection CollectionI
...
Mit $myForm->bind($myTypeFoo) binde ich das Objekt ans Formular und der Input wird automatisch ins Objekt hydriert. Dank bind(...) erhalte ich aus dem Formular ein fertiges TypeFoo-Objekt. Ebenso funktioniert es in der entgegengesetzten Richtung: Zum Editieren eines einmal persistierten Objekts hole ich es aus der Datenbank, übergebe dem Formular und erhalte ein fertiges Formular mit vorausgefüllten Feldern.

Alles funktionierte bestens, bis ein MultiCheckbox Element ins Spiel kam. Die Objekt-Property dahinter ist ein Array mit Elementen vom Typ Protocol (jedes Protocol hat Eigenschaften id und name, die für die MultiCheckBox entsprechend als value und label dienen sollen). Beim Speichern muss ich tricksen: Ich bekomme nämlich nicht mehr das fertige Objekt aus dem Formular. Die Property protocols ist nämlich nicht, wie eigenlicht designt, ein Array von Protocol-Objekten, sondern ein Array von value=>label Arrays, aus dem ich dann in einem Zwischenschritt "manuell" ein Array von Protocol-Objekten erstelle, um das TypeFoo-Objekt zu komplettieren und an das Model zum Persistieren zu übergeben. (Hässlicher Workaround, aber funktioniert.) Das eigentliche Problem tritt aber beim Edit-Formular auf. Der FormMultiCheckBox View Helper erwartet fir die MultipleCheckbox einfache Werte und versucht die Protocol-Objekte im Array von Element#options['value_options'] zu finden, was natürlich nicht klappt und nur zu einer Notice führt:

Notice: Object of class My\DataObject\Protocol could not be converted to int in /var/www/path/to/project/vendor/zendframework/zend-form/src/View/Helper/FormMultiCheckbox.php on line 202
Da stößt mein oben beschriebener hässlicher Workaround wohl an seine Grenzen und muss verworfen werden.

Wie kann man das sauber lösen und ein Array von Objekten für ein MultiCheckBox Element nutzen?

Danke schon mal!
 
Zuletzt bearbeitet von einem Moderator:

Kaiuwe

Super-Moderator
Hässlicher Workaround, aber funktioniert.
Scheinbar funktioniert dieser nicht, denn ansonsten hättest du keine Probleme. :p

Grundsätzlich ist der Vorgang beim Formularen wie folgt:

  1. Objekt wird an Formular gebunden
  2. Hydrator extrahiert die Daten aus dem Objekt
  3. Werte können für die einzelnen Formularfelder gesetzt werden
Dein Problem liegt also beim 2. Punkt. Wie sieht denn dein Hydrator dazu aus?
 

automatix

New member
Messerscharf analysiert! :)

Ja, das Problem liegt im Hydrator. Aber die Lösung gefällt mir nicht. Die läge nämlich darin, einen speziellen Hydrator für das (bzw. jedes) MultiCheckbox-Feld zu schreiben. Und bei dem Hydrator müsste die extract(...) -- anders als bei gewöhnlichen HydratorInterface-Implementierungen -- anstatt eines Arrays mit den Objekt-Daten, einen Integer-Wert (in dem Fall die $id) liefern. Das designtechnisch aus zwei Gründen bedenklich:

1. Wenn man ohnehin schon einen Hydrator für Protocol hat, muss man einen zweiten speziell für den MultiCheckbox-Fall schreiben.

2. Wie schon oben angesprochen, verhält sich dann die ProtocolHydrator#extract(...) nicht wie normalerweise erwartet und bricht das HydratorInterface. Zwar nicht formell, weil der Ausgabe-Typ nicht zur Signatur der Methode gehört, aber jedenfalls logisch.

Hier eine mögliche Implementierung:

FooHydratorFactory

PHP:
namespace My\Hydrator\Factory;
use Zend\ServiceManager\FactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
use My\DataObject\Protocol;
use My\Hydrator\Strategy\Entity\GenericCollectionStrategy;
class FooHydratorFactory implements FactoryInterface
{
    public function createService(ServiceLocatorInterface $serviceLocator)
    {
        $fooHydrator = $serviceLocator->get('Zend\Hydrator\ClassMethods');
        $protocolHydrator = $serviceLocator->get('My\Hydrator\ProtocolHydrator');
        $fooHydrator->addStrategy('protocols', new GenericCollectionStrategy($protocolHydrator, new Protocol()));
        // no naming map
        return $fooHydrator;
    }
}
ProtocolHydratorFactory

PHP:
namespace My\Hydrator\Factory;
use Zend\ServiceManager\FactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
use My\Hydrator\ProtocolHydrator;
class ProtocolHydratorFactory implements FactoryInterface
{
    public function createService(ServiceLocatorInterface $serviceLocator)
    {
        return new ProtocolHydrator();
    }
}
ProtocolHydrator

PHP:
namespace My\Hydrator;
use Zend\Hydrator\ClassMethods;
use My\DataObject\Protocol;
class ProtocolHydrator extends ClassMethods
{
    public function extract($object)
    {
        return $object->getId();
    }
    public function hydrate(array $data, $object)
    {
        // An array with one int value for the ID is expected, e.g.:
        // [123]
        $dataValues = array_values($data);
        $id = array_shift($dataValues);
        return parent::hydrate(['id' => $id], $object);
    }
}
GenericCollectionStrategy

PHP:
use Zend\Hydrator\Strategy\StrategyInterface;
use Zend\Hydrator\HydratorInterface;
class GenericCollectionStrategy implements StrategyInterface
{
    private $hydrator;
    private $prototype;
    // constructor
    public function getPrototype()
    {
        return $this->prototype ? clone $this->prototype : null;
    }
    // getters, setters...
    public function extract($objects)
    {
        $collection = [];
        if (is_array($objects) || $objects instanceof \Traversable) {
            foreach ($objects as $object) {
                $prototypeClass = get_class($this->prototype);
                $collection[] = is_object($object) && $object  instanceof $prototypeClass ? $this->hydrator->extract($object) :  $object;
            }
        }
        return $collection;
    }
    public function hydrate($array)
    {
        $collection = [];
        if (is_array($array)) {
            foreach ($array as $element) {
                // the original clean variant
                // $collection[] = is_array($element) ? $this->hydrator->hydrate($element, $this->getPrototype()) : $element;
                // the variant for non-array elements
                $element = is_array($element) ? $element : [$element];
                $collection[] = $this->hydrator->hydrate($element, $this->getPrototype());
            }
        }
        return $collection;
    }
}
Das funktioniert, aber schön bzw. sauber ist anders.

UPDATE

Ich sagte, die Lösung ist nicht schön. Und wie es bei unschönen Lösungen meistens ist, verletzen sie nicht nur das feine Ästhetik-Gefühl des Entwicklers, sondern führen früh oder spät zu Problemen. Also, diese Lösung hat die Einschränkung, dass das Hydrating (i.S.d. HydratorInterface#hydrate(...)) nicht funktioniert. Damit es geht, musste die GenericCollectionStrategy#hydrate(...) angepasst und die ProtocolHydrator#hydrate(...) auf eine fast bizarre Weise implementiert werden.

Ich sehe das alles weniger als eine Lösung, sondern eher als einen Workaround.

Wie kann das Problem eleganter -- also unter Vermeideung dieses ganzen Gefrickels -- gelöst werden?
 
Zuletzt bearbeitet:

gbell12

New member
Sorry for the English, but I thought it would be helpful if I mentioned the doctrine-module for ZF has a 'ObjectMultiCheckbox' meant to solve this problem.
As a new user, I'm not able to post the link.
 
Oben